Přejít na hlavní obsah
← CutOptim Engine API

Reference API

Základní URL https://api.cutoptim.com · verze kontraktu v1

Klíč si vytvořte na své nástěnce a pak jej posílejte jako Authorization: Bearer <key>.

Specifikace OpenAPI: /engine/openapi.json

Endpointy

POST/v1/optimize/2doptimalizace 2D desek
POST/v1/optimize/1doptimalizace 1D / lineárního materiálu (tyče, profily, trubky)
POST/v1/optimize/woodoptimalizace dřeva — 1D s přiřazením průřezu
POST/v1/optimize/nestnesting podle skutečného tvaru — nepravidelné polygony na pevných deskách, s vyloučenými zónami (laser / plazma / vodní paprsek)
POST/v1/validate/2dvalidace požadavku bez výpočtu — zdarma, bez klíče, bez kvóty (2d / 1d / wood / nest)
POST/v1/validate/1dvalidace požadavku bez výpočtu — zdarma, bez klíče, bez kvóty (2d / 1d / wood / nest)
POST/v1/validate/woodvalidace požadavku bez výpočtu — zdarma, bez klíče, bez kvóty (2d / 1d / wood / nest)
POST/v1/validate/nestvalidace požadavku bez výpočtu — zdarma, bez klíče, bez kvóty (2d / 1d / wood / nest)
GET/v1/jobs/{id}dotázání na asynchronní úlohu jádra max — vrací její status a po dokončení i plán (bez kvóty; jen vaše vlastní úlohy)
POST/v1/import/nestnačte obrysy dílů ze souboru SVG nebo DXF — vyžaduje klíč, nespotřebovává kvótu
GET/v1/usagespotřeba a kvóta ÚČTU v aktuálním měsíci
GET/v1/healthkontrola dostupnosti — bez klíče, bez rate limitu, bez databáze

POST /v1/validate/{2d,1d,wood,nest} — response

validbooleanAlways true on a 200 — a malformed body is a 400 with the exact bad field path instead. The body is the SAME one the matching optimize endpoint takes.
modestring"2d" | "1d" | "wood" | "nest" — the endpoint you called.
contractVersionstringShape version. Currently "1".
engineEnabledbooleanNEST ONLY — whether the nesting engine is built into this deployment. Absent for the rectangular modes.
partsobject{ rows, total } — part rows sent, and the total quantity after qty expansion. Check it against the part cap before you spend a call.
stockobject{ rows, total } — the same for stock.
warningsstring[]Approximate, SOLVE-FREE feasibility notes — e.g. a part that fits no stock (2D and nest: bounding box; wood: no matching section long enough). Empty ⇒ every part fits something. Ignores trim, kerf and material: a shape check, not a solve.

Jednotky

API je vůči jednotkám neutrální. Zvolte si jednu jednotku — milimetry, palce, cokoli — používejte ji pro každé číslo, které posíláte, a každé číslo, které dostanete zpátky, bude ve stejné jednotce. Na serveru se nic nepřepočítává a žádný název pole žádnou jednotku nepředepisuje.

Platí to pro rozměry dílů i materiálu, kerf, tolerance, trim a minOffcut na vstupu a pro každou souřadnici, pozici, zbývající délku, zbytek i délku řezu na výstupu. Míchání jednotek v jednom požadavku vytvoří plán, který projde validací a přitom je fyzicky špatný — a server to nemá jak poznat.

Souřadný systém

Počátek je levý horní roh desky: x roste doprava po šířce desky, y roste dolů po výšce desky. x a y dílu jsou jeho levý horní roh a w a h jsou rozměry tak, jak je díl umístěný — už prohozené, když je rotated true — takže obdélník x, y, w, h je přímo obrys na desce, bez dalšího přepočítávání. Obdélníky zbytků používají stejný systém.

Ořez posouvá umístění: trim.left posune každý díl doprava a trim.top každý díl dolů, protože díly se rozmisťují do využitelné plochy a ta se pak přesouvá zpátky na celou desku. trim.right a trim.bottom využitelnou plochu zmenšují, ale počátek neposouvají. w a h desky jsou vždy plné rozměry materiálu, včetně ořezu — a právě proto se ořez v yieldPct počítá jako odpad.

2D — požadavek

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"]
}

Pole dílu

w, hpovinné — rozměry dílu
qtyvýchozí 1 — rozepisuje se na serveru; počítá se do limitu 2,000
namenepovinné označení, vrací se u každého umístění
rotatablevýchozí true — smí se díl otočit o 90°
grainGroupčlenové skupiny zůstanou na jedné desce (shoda vzoru dřeva)
prioritymusí být vyříznut: při omezeném materiálu má přednost na desce (spolu s respectStock)
edgeBandingolepení hran po jednotlivých stranách: uveďte odkaz na typ u kterékoli z top / right / bottom / left (libovolný řetězec, váš vlastní kód) — odpověď sečte metry podle odkazu. Jen metadata, dílem nikdy nepohne. Jen 2D
materialznačka materiálu (libovolný řetězec, váš vlastní kód): díly a materiál stejného druhu se rozmisťují jen společně. Na rozdíl od edgeBanding mění rozvržení. Chybí = jeden neurčený fond. Funguje v každém režimu

Pole materiálu

w a h jsou povinné. qty má výchozí hodnotu 1 a pevným limitem je jen ve spojení s respectStock. price je cena za desku a vstupuje do totalPrice a do režimu nákladů. priority (boolean) spotřebuje tento materiál nejdřív; material (libovolný řetězec) ho omezí na díly stejného materiálu.

Volby

kerfšířka pilového kotouče (výchozí 0)
tolerancepřijme řezy, které přesahují nejvýše o tuto hodnotu
trimořez hran po jednotlivých stranách: left, right, top, bottom
firstCut'auto' | 'horizontal' | 'vertical' (výchozí 'auto')
minimizeCosthodnotí kandidáty podle nejnižší celkové ceny materiálu; při více oceněných rozměrech materiálu je kombinuje (2D) nebo volí nejlevnější délku (1D), aby snížil účet, i když se tím spotřebuje více materiálu
respectStockbere qty každého řádku materiálu jako pevný limit
minOffcutvrací jen zbytky, jejichž krátká strana je alespoň takto velká
maxCutStageslimit fází pro formátovací pilu — počet fází, ne hloubka stromu řezů
minimizeRotationspreferuje rozvržení, která otáčejí méně dílů
effort'fast' | 'balanced' (výchozí 'balanced'). Hloubka hledání: 'balanced' provede úplné best-of s více strategiemi; 'fast' vynechá jedno nákladné hledání kombinací ploch na desku — výrazně rychlejší u velkých zakázek za cenu několika bodů využití, stále gilotinový a nikdy hustší než 'balanced'. U malých zakázek je výsledek obvykle totožný. Jen jádro heuristic

include dělá dvě věci. ZÚŽENÍ — "cutPlan" a "offcuts" jsou zapnuté ve výchozím stavu; přítomné pole ponechá jen ta zužovací klíčová slova, která vyjmenuje (prázdné pole obě vypustí). PŘIDÁVANÝ EXPORT — "svg", "csv" a "dxf" každý přidá do odpovědi příslušný export jako ŘETĚZEC: svg samostatný 2D výkres rozvržení (jen 2D — požadavek 1d/wood místo něj vrátí upozornění), dxf výkres R12/AC1009 na vrstvách STOCK/PARTS/LABELS, csv seznam řezů. Exportní klíčová slova zúžení neovlivňují, takže include:["svg"] přidá svg a — protože neuvádí žádné zužovací klíčové slovo — vypustí cutPlan/offcuts; použijte ["cutPlan","offcuts","svg"], chcete-li zachovat vše a přidat svg.

parts[].meta · stock[].meta — passthrough (2d · 1d · wood · nest)

parts[].metaobjectOpaque JSON of your own — an ERP article number, an order-line id, a customer ref. Echoed VERBATIM on every placed piece of that part. The optimizer never reads it, so it can never change a layout.
stock[].metaobjectThe same on a stock row: echoed on every sheet / rod / nested sheet cut from it, so the plan reconciles with your system without a lookup table.

Volba effort vyvažuje čas výpočtu proti využití. Tady je ten kompromis, změřený na jedné náročné zakázce — každé číslo pochází ze skutečného packeru.

Přepínač effort: využití materiálu vs čas výpočtuPřepínač effort: využití materiálu vs čas výpočtu. fast: 350 desky · 76.2% · ≈1.9 s. balanced: 330 desky · 80.8% · ≈4.8 s. max: rezervováno — hustěji chce pomalejší hledání. U většiny (menších) zakázek jsou oba totožné; rozdíl se objeví jen u velkých zakázek, jako je tato. balanced je výchozí a nikdy není hustší, než dokáže fast.Přepínač effort: využití materiálu vs čas výpočtuJedna náročná zakázka — zhruba 1 550 dílů na desce 2,07 × 5,6 m. Každé číslo změřené na skutečném packeru.74%76%78%80%82%84%02 s4 s6 sčas výpočtu · rychleji →využití materiálu · hustěji ↑⇄ přepínač effort+4,6 p.b. využití · −20 desek−5,7 % materiálu · ≈2,5× pomalejšífast350 desky · 76.2% · ≈1.9 s★ balanced · výchozí330 desky · 80.8% · ≈4.8 smaxrezervovánohustěji chcepomalejší hledání
U většiny (menších) zakázek jsou oba totožné; rozdíl se objeví jen u velkých zakázek, jako je tato. balanced je výchozí a nikdy není hustší, než dokáže fast.

A tak jako tak je to rychlé: i největší výrobní zakázky — 2000 dílů a více — se na výchozím enginu vyřeší v řádu sekund, pohodlně v časovém rozpočtu API.

2D — odpověď

{
  "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 sdružuje řezy ležící na stejné souřadnici (jedno nastavení dorazu); sawPasses počítá každý průjezd. Dvě poctivé míry téhož plánu, ne tvrzení, že se shodují s číslem některého konkurenta.
  • guillotineValid / cutPlan — Když rozvržení nelze rozřezat průchozími řezy od hrany k hraně, je guillotineValid false a cutPlan null. To je skutečná informace — na formátovací pile se vyrobit nedá — ne chyba.
  • unplaced + warnings — Nesplnitelná zakázka vrací 200, díly jsou vypsané v unplaced a poznámka je ve warnings. Plán, se kterým se dá pracovat, je lepší než stavový kód.
  • edgeBanding — Když některý díl nese edgeBanding, odpověď přidá blok edgeBanding: běžné metry, které spotřebuje každý odkaz na typ, na díl i jako součet za celou zakázku. Je to přesná geometrie bez přídavku na odpad — vlastní přídavek si přidá dílna — a předpokládá vstup v milimetrech (÷1000 na metry). U zakázky bez olepení klíč zcela chybí.
  • materials / unmatchedMaterials — Když některý díl nebo materiál nese material, odpověď přidá materials (souhrn po jednotlivých materiálech — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) a unmatchedMaterials (poptávka, jejíž materiál nemá odpovídající zásobu). U dřeva se materiál nese u každé sekce průřezu. Oba klíče u zakázky bez materiálu chybí a ta zůstává bajtově identická.

Pole odpovědi

Názvy polí a jejich typy jsou součástí kontraktu, takže tabulky níže zůstávají ve všech jazycích v angličtině — přeložený název pole by dokumentoval API, které neexistuje.

POST /v1/optimize/2d — top level

enginestringWhich engine ran: "heuristic", "balanced" or "max".
engineVersionstringAlgorithm identity — package version + a content hash of the algorithm source. Moves automatically on any packer change, and independently per engine.
contractVersionstringShape version, matching the /v1/ in the path. Currently "1".
deterministicbooleanAlways true. Present so a client can assert the guarantee it relies on.
sheetsarrayOne entry per sheet used, in cutting order.
metricsobjectAggregate numbers for the whole job.
cutPlanarray | nullThe sawing plan. null when guillotineValid is false; absent when excluded via include.
unplacedarrayParts that did not fit, aggregated by name + size. Empty when everything fitted.
warningsstring[]Free-text notes about the plan. Do not parse — branch on unplaced, guillotineValid and metrics.
guillotineValidbooleanTrue when the layout is producible with edge-to-edge cuts, i.e. on a panel saw.
edgeBandingobjectLinear metres of edge banding, grouped by type reference. ABSENT unless a part requested banding via parts[].edgeBanding. 2D only.
materialsarrayPer-material rollup. ABSENT unless a part or stock row carried material — a material-free job stays byte-identical. (OPEN-256)
unmatchedMaterialsarrayDemand whose material has no matching stock at all. ABSENT when it does not happen. A missing-material report, not a did-not-fit one.
svgstringInline SVG of the 2D layout (self-contained, no external refs). Present ONLY when include contains "svg". 2D only. (OPEN-223)
csvstringInline CSV cut list. Present ONLY when include contains "csv".
dxfstringInline DXF (R12/AC1009) on layers STOCK/PARTS/LABELS. Present ONLY when include contains "dxf".
timing.solveMsnumberMilliseconds inside the packer, 2 decimals. Excludes parsing, auth and queueing.

sheets[]

w, hnumberFULL stock dimensions, trim included — not the usable area.
pricenumber | nullPrice of the stock row this sheet came from, or null if none was given.
partsarrayPlacements on this sheet.
offcutsarrayUsable leftover rectangles, filtered by options.minOffcut. Absent (not empty) when excluded via include.
metaobjectPresent only when the stock row carried meta — echoed verbatim from stock[].meta. (OPEN-224)

sheets[].parts[]

namestringThe requested name, or the generated default "Part <row>".
x, ynumberTop-left corner of the part, from the top-left corner of the sheet.
w, hnumberDimensions AS PLACED — already swapped when rotated is true. No client-side swap needed.
rotatedbooleanTrue if the part was turned 90° from the requested w×h. Informational only.
metaobjectPresent only when the part carried meta — echoed verbatim from parts[].meta on every placed piece. (OPEN-224)

sheets[].offcuts[]

x, y, w, hnumberA leftover rectangle, in the same coordinate system as the placements.

metrics (2D)

sheetCountintegerSheets used. Equals sheets.length.
yieldPctnumberPlaced part area ÷ total FULL sheet area × 100, 2 decimals. Trim and kerf count as waste.
placedintegerPieces placed, after qty expansion.
totalintegerPieces requested, after qty expansion.
cutLinesintegerCollinear cuts merged: same axis, same coordinate, same stage counted once — one fence setting.
sawPassesintegerEvery cut, one per strip crossed — how many separate passes the saw makes.
cutLengthnumberTotal distance sawn, 3 decimals. Same under either counting convention. In your unit.
totalPricenumberSum of the used sheets’ prices, 2 decimals. 0 when no stock row carried a price.

unplaced[] (2D)

namestringThe part name.
w, hnumberAs requested.
qtyintegerHow many of this part could not be placed.

edgeBanding (2D — present only when a part is banded)

totalMetersnumberOrder-wide total across every banded edge, 3 decimals. ⚠️ Assumes mm input: top/bottom edges run the part width, left/right the height, ×qty, ÷1000. In another unit it is your raw edge length ÷ 1000.
byType[]{ reference, meters }Order total split by the caller-supplied type reference, sorted by reference.
byPart[]{ name, meters, byType }One entry per requested part row that has any banded edge; its byType splits that part’s metres by reference.

cutPlan[]

sheetinteger0-based index into sheets (2D) or rods (1D). Named sheet in both.
stepinteger1-based order within THIS sheet — it restarts at 1 on every sheet.
axis"h" | "v""h": blade travels along x, separating top from bottom. "v": along y, separating left from right.
posnumberThe blade’s LOW-COORDINATE edge — the y value for "h", the x value for "v" — not its centre line. The kerf occupies pos to pos + kerf.
lengthnumberDistance the blade travels on this cut: the extent of the region crossed. Always 0 in 1D.
stageinteger1-based machine pass. Increments only when the axis flips relative to the parent cut.

2D · edgeBanding

Když některý díl nese edgeBanding, odpověď přidá blok edgeBanding: běžné metry, které spotřebuje každý odkaz na typ, na díl i jako součet za celou zakázku. Je to přesná geometrie bez přídavku na odpad — vlastní přídavek si přidá dílna — a předpokládá vstup v milimetrech (÷1000 na metry). U zakázky bez olepení klíč zcela chybí.

POST /v1/optimize/2d

{
  "parts": [
    {
      "name": "Door",
      "w": 600,
      "h": 400,
      "qty": 2,
      "edgeBanding": {
        "top": "ABS oak 22",
        "bottom": "ABS oak 22",
        "left": "ABS white 22",
        "right": "ABS white 22"
      }
    },
    { "name": "Shelf", "w": 800, "h": 300, "edgeBanding": { "top": "ABS oak 22" } }
  ],
  "stock": [{ "w": 2440, "h": 1220, "qty": 10, "price": 42 }],
  "options": { "kerf": 3 }
}
{
  "edgeBanding": {
    "totalMeters": 4.8,
    "byType": [
      { "reference": "ABS oak 22", "meters": 3.2 },
      { "reference": "ABS white 22", "meters": 1.6 }
    ],
    "byPart": [
      {
        "name": "Door",
        "meters": 4,
        "byType": [
          { "reference": "ABS oak 22", "meters": 2.4 },
          { "reference": "ABS white 22", "meters": 1.6 }
        ]
      },
      {
        "name": "Shelf",
        "meters": 0.8,
        "byType": [{ "reference": "ABS oak 22", "meters": 0.8 }]
      }
    ]
  }
}

material

Když některý díl nebo materiál nese material, odpověď přidá materials (souhrn po jednotlivých materiálech — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) a unmatchedMaterials (poptávka, jejíž materiál nemá odpovídající zásobu). U dřeva se materiál nese u každé sekce průřezu. Oba klíče u zakázky bez materiálu chybí a ta zůstává bajtově identická.

POST /v1/optimize/2d

{
  "parts": [
    { "name": "Door", "w": 600, "h": 400, "qty": 4, "material": "MDF 18" },
    { "name": "Shelf", "w": 800, "h": 300, "qty": 6, "material": "Oak 18" },
    { "name": "Back panel", "w": 1000, "h": 500, "qty": 2, "material": "Ply 6" }
  ],
  "stock": [
    { "w": 2440, "h": 1220, "qty": 10, "price": 42, "material": "MDF 18" },
    { "w": 2440, "h": 1220, "qty": 10, "price": 68, "material": "Oak 18", "priority": true }
  ],
  "options": { "kerf": 3 },
  "engine": "heuristic"
}
{
  "materials": [
    {
      "material": "MDF 18",
      "sheetCount": 1,
      "yieldPct": 32.25,
      "placed": 4,
      "total": 4,
      "totalPrice": 42
    },
    {
      "material": "Oak 18",
      "sheetCount": 1,
      "yieldPct": 48.37,
      "placed": 6,
      "total": 6,
      "totalPrice": 68
    }
  ],
  "unmatchedMaterials": [
    {
      "material": "Ply 6",
      "parts": [{ "name": "Back panel", "w": 1000, "h": 500, "qty": 2 }]
    }
  ]
}

materials[] — per-material rollup (2d · 1d · nest)

materialstringThe tag exactly as you sent it. Free text, matched exactly.
sheetCount | rodCountintegerStock consumed for this material — sheetCount on 2D and nest, rodCount on 1D.
yieldPct | densitynumberThis material’s own fill — yieldPct on the rectangular modes, density on nest (a polygon fill, not comparable to yieldPct).
placed, totalintegerPieces placed and requested for this material, after qty expansion.
totalPricenumberSum of the prices of the stock used for this material.

unmatchedMaterials[] — demand with no matching stock

materialstringThe tag that has no stock of its own anywhere in the request.
partsarrayThe demand rows in that material, in the mode’s unplaced shape: name, w, h, qty on 2D; name, length, qty on 1D; name, qty on nest.

cutPlan — co jeden krok fyzicky znamená

Jeden krok je jeden pohyb kotouče a seznam je v tom pořadí, ve kterém se skutečně dá řezat: nadřazený řez stojí před řezy uvnitř kusu, který jím vznikl, protože pásek nemůžete zkrátit napříč, dokud ho neodříznete. axis "h" znamená, že kotouč jede podél x a odděluje horní část od dolní; axis "v" znamená, že jede podél y a odděluje levou část od pravé. pos je NIŽŠÍ hrana kotouče z hlediska souřadnic — hodnota y pro "h", hodnota x pro "v" — nikoli jeho osa: šířka řezu (kerf) zabírá úsek od pos do pos + kerf, takže kotouč ubírá materiál ve směru, ve kterém souřadnice roste — u "h" tedy dolů a u "v" doprava. Materiál na nižší straně řezné linie — u "h" nad ní, u "v" vlevo od ní — je ten kus, který daný řez uvolní. length je vzdálenost, kterou kotouč na tom jednom řezu ujede: rozsah oblasti, kterou přejíždí, ne šířka celé desky.

stage je jeden průjezd stroje. Začíná na 1 a zvyšuje se jen tehdy, když se axis oproti nadřazenému řezu překlopí — takže rozříznutí desky na šest pásků je jedna fáze a jejich příčné zkrácení je fáze další. Přesně takto rozumí „třífázovému řezání“ formátovací pila; není to hloubka stromu řezů a je to právě to, co omezuje maxCutStages. sheet je index do sheets počítaný od nuly a step začíná na každé desce znovu od 1, místo aby běžel přes celou zakázku.

cutPlan je null — ne chybějící, ne prázdný — vždy, když je guillotineValid false: rozvržení, které nelze rozřezat průchozími řezy od hrany k hraně, nemá žádnou sekvenci řezů, kterou by šlo vrátit. Z odpovědi zmizí úplně jen v případě, že jste jej vynechali z include.

1D — lineární

POST /v1/optimize/1d

{
  "parts": [{ "name": "Rail", "length": 1200, "qty": 6 }],
  "stock": [{ "length": 3000, "qty": 5, "price": 12.5 }],
  "options": { "kerf": 3, "trim": { "start": 10, "end": 0 } }
}

Díly mají length (plus qty, name, priority); materiál má length, qty a price. Díly i zásoba navíc přijímají volitelnou značku material (zásoba i priority) — material omezí díl na zásobu stejného materiálu a odpověď pak přidá materials a unmatchedMaterials jako u 2D. Volby jsou kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock a minOffcut. Odpověď vrací rods místo sheets, každou s vlastními díly, zbývající délkou a zbytky.

1D — odpověď

{
  "engine": "heuristic",
  "engineVersion": "1.0.0+10e0c941",
  "contractVersion": "1",
  "deterministic": true,
  "rods": [
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    },
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    },
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    }
  ],
  "metrics": {
    "rodCount": 3,
    "yieldPct": 80,
    "placed": 6,
    "total": 6,
    "cuts": 6,
    "totalPrice": 37.5,
    "toleranceAcceptedCount": 0
  },
  "unplaced": [],
  "warnings": [],
  "timing": { "solveMs": 2.19 },
  "cutPlan": [
    { "sheet": 0, "step": 1, "axis": "v", "pos": 1210, "length": 0, "stage": 1 },
    { "sheet": 0, "step": 2, "axis": "v", "pos": 2413, "length": 0, "stage": 1 },
    { "sheet": 1, "step": 1, "axis": "v", "pos": 1210, "length": 0, "stage": 1 },
    { "sheet": 1, "step": 2, "axis": "v", "pos": 2413, "length": 0, "stage": 1 },
    { "sheet": 2, "step": 1, "axis": "v", "pos": 1210, "length": 0, "stage": 1 },
    { "sheet": 2, "step": 2, "axis": "v", "pos": 2413, "length": 0, "stage": 1 }
  ]
}

rods nahrazuje sheets a guillotineValid tady není, protože lineární řez je vždy vyrobitelný. pos každého dílu je odstup jeho bližšího konce od toho konce tyče, který ořezává trim.start, takže první díl začíná přesně na trim.start a každé další pos přidává jednu šířku řezu (kerf). remaining je VYUŽITELNÝ zbytek: šířka řezu, kterým se odděluje od posledního kusu, je už odečtena, takže je to znovupoužitelná délka, ne holá mezera. Uvádí se u každé tyče, i když je menší než minOffcut — minOffcut filtruje pouze pole offcuts, které obsahuje nejvýše jeden záznam. V plánu řezání je sheet index tyče, axis je vždy "v", stage je vždy 1 a length je vždy 0: příčné zkrácení tyče nemá žádnou dráhu, kterou by šlo hlásit, a proto také 1D metrics obsahují cuts, ale žádné cutLength.

Dřevo — průřez

POST /v1/optimize/wood

{
  "parts": [
    { "name": "Rafter", "sw": 50, "sh": 100, "length": 2400, "qty": 2 },
    { "name": "Noggin", "sw": 50, "sh": 100, "length": 600, "qty": 4 },
    { "name": "Beam", "sw": 50, "sh": 150, "length": 3000, "qty": 2 }
  ],
  "stock": [
    { "name": "C24 50x100", "sw": 50, "sh": 100, "length": 4000, "qty": 5, "price": 12.5 },
    { "name": "C24 50x150", "sw": 50, "sh": 150, "length": 4000, "qty": 3, "price": 18 }
  ],
  "options": { "kerf": 3 }
}

Dřevo má identitu, kterou holá tyč nemá: díl 50×150 z materiálu 50×100 nevyřežete, ať zbývá jakákoli délka. Díly i materiál proto nesou sw a sh, dvě strany průřezu, v libovolném pořadí — 50×100 a 100×50 je tentýž otočený hranol a patří do jednoho průřezu. Zakázka se rozdělí podle průřezu, každý průřez se přiřadí vlastnímu materiálu a řeší se samostatně, a jedno volání vrátí celek. Díly i zásoba navíc přijímají volitelnou značku material (zásoba i priority): s ní se dub 50×100 a borovice 50×100 stanou dvěma samostatnými sekcemi a každá sekce nese svůj materiál. Volby jsou stejné jako u 1D.

Dřevo — odpověď

{
  "engine": "heuristic",
  "engineVersion": "1.0.0+7c1f3a62",
  "contractVersion": "1",
  "deterministic": true,
  "sections": [
    {
      "section": "50x100",
      "sw": 50,
      "sh": 100,
      "stockName": "C24 50x100",
      "rods": [
        {
          "length": 4000,
          "price": 12.5,
          "remaining": 391,
          "parts": [
            { "name": "Rafter", "pos": 0, "length": 2400 },
            { "name": "Noggin", "pos": 2403, "length": 600 },
            { "name": "Noggin", "pos": 3006, "length": 600 }
          ],
          "offcuts": [391]
        },
        {
          "length": 4000,
          "price": 12.5,
          "remaining": 391,
          "parts": [
            { "name": "Rafter", "pos": 0, "length": 2400 },
            { "name": "Noggin", "pos": 2403, "length": 600 },
            { "name": "Noggin", "pos": 3006, "length": 600 }
          ],
          "offcuts": [391]
        }
      ],
      "metrics": {
        "rodCount": 2,
        "yieldPct": 90,
        "placed": 6,
        "total": 6,
        "cuts": 6,
        "totalPrice": 25
      },
      "unplaced": []
    },
    {
      "section": "50x150",
      "sw": 50,
      "sh": 150,
      "stockName": "C24 50x150",
      "rods": [
        {
          "length": 4000,
          "price": 18,
          "remaining": 997,
          "parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
          "offcuts": [997]
        },
        {
          "length": 4000,
          "price": 18,
          "remaining": 997,
          "parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
          "offcuts": [997]
        }
      ],
      "metrics": {
        "rodCount": 2,
        "yieldPct": 75,
        "placed": 2,
        "total": 2,
        "cuts": 2,
        "totalPrice": 36
      },
      "unplaced": []
    }
  ],
  "unmatched": [],
  "metrics": {
    "sectionCount": 2,
    "rodCount": 4,
    "yieldPct": 82.5,
    "placed": 8,
    "total": 8,
    "cuts": 8,
    "totalPrice": 61,
    "toleranceAcceptedCount": 0
  },
  "unplaced": [],
  "warnings": [],
  "timing": { "solveMs": 2.96 },
  "cutPlan": [
    {
      "section": "50x100",
      "sheet": 0,
      "step": 1,
      "axis": "v",
      "pos": 2400,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 0,
      "step": 2,
      "axis": "v",
      "pos": 3003,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 0,
      "step": 3,
      "axis": "v",
      "pos": 3606,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 1,
      "step": 1,
      "axis": "v",
      "pos": 2400,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 1,
      "step": 2,
      "axis": "v",
      "pos": 3003,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 1,
      "step": 3,
      "axis": "v",
      "pos": 3606,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x150",
      "sheet": 0,
      "step": 1,
      "axis": "v",
      "pos": 3000,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x150",
      "sheet": 1,
      "step": 1,
      "axis": "v",
      "pos": 3000,
      "length": 0,
      "stage": 1
    }
  ]
}

sections nahrazuje rods na nejvyšší úrovni: každá položka je jeden průřez s vlastními rods (tvarem shodnými s 1D) a vlastními metrics, takže hodnoty za materiál máte bez dopočítávání. unmatched nemá v 1D obdobu — je to poptávka, pro jejíž průřez jste nezadali žádný materiál. To je jiný problém než unplaced (díly, které materiál měly a nevešly se) a řeší se jinak, proto se ty dva nikdy nemíchají. metrics.total počítá každý požadovaný kus, včetně těch v unmatched. V plánu řezů každý krok navíc uvádí svou section a sheet je index tyče UVNITŘ daného průřezu, ne čítač přes celou zakázku.

POST /v1/optimize/wood — top level (what differs from 1D)

sectionsarrayReplaces rods at the top level: one entry per cross-section, each matched to its own stock and solved on its own.
unmatchedarrayDemand whose cross-section has NO stock at all. Deliberately separate from unplaced (had stock, did not fit) — the fix differs, so the two are never mixed.
unplacedarrayDid-not-fit demand, aggregated across the sections that DID have stock. Same shape as 1D.
metricsobjectJob-wide totals across every section (below).
cutPlanarray | nullAs 1D, except every step also carries section, and sheet is the rod index WITHIN that section, not a job-wide counter.
csv, dxfstringInline export, present only when include names the token. svg is 2D-only — a wood request asking for it gets a warning instead.

sections[]

sectionstringNormalised cross-section key, e.g. "50x100" — short side first, so 50×100 and 100×50 are one section.
sw, shnumberThe two cross-section sides: sw the SHORT one, sh the long one, whatever order they arrived in.
stockNamestring | nullThe name of the stock row this section was matched to, or null when that row carried none.
materialstringPresent only when the section carries a material — an oak 50×100 and a pine 50×100 are two sections. (OPEN-256)
rodsarrayIdentical in shape to the 1D rods[] above, meta and offcuts included.
metricsobjectThis section’s own totals: rodCount, yieldPct, placed, total, cuts, totalPrice — so the per-section figures need no recomputation.
unplacedarrayThis section’s parts that had stock and still did not fit.

metrics (wood)

sectionCountintegerCross-sections solved. Equals sections.length.
rodCountintegerBars used across every section.
yieldPctnumberPlaced length ÷ total FULL bar length × 100 over the whole job, 2 decimals.
placedintegerPieces placed, after qty expansion.
totalintegerPieces REQUESTED, after qty expansion — unmatched ones included.
cutsintegerCrosscuts across every used bar.
totalPricenumberSum of the used bars’ prices, 2 decimals.
toleranceAcceptedCountintegerPieces that fitted only because options.tolerance allowed an overshoot.

unmatched[]

sectionstringThe cross-section key nothing in stock matched.
sw, shnumberThat cross-section’s two sides.
materialstringPresent when the cross-section DOES exist in stock but only in a different material. (OPEN-256)
partsarrayThe demand rows in that section, in the 1D unplaced shape: name, length, qty.
qtyintegerTotal pieces in this section that had no stock at all.

Nesting podle skutečného tvaru

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 }]
    }
  ]
}

Tři výše uvedené režimy rozmisťují obdélníky. POST /v1/optimize/nest rozmisťuje LIBOVOLNÉ POLYGONY: díl je obrys (polygon, s volitelnými vnitřními otvory), ne šířka×výška, takže se díly do sebe zaklesávají v konkávních kapsách a vzduch ve výřezech, který opsaný obdélník promrhá, se získá zpět — na reprezentativní zakázce 6 desek tam, kde tytéž díly podle opsaného obdélníku potřebují 9. Je to jiná třída algoritmu (geometrické kolizní jádro, ne gilotinový packer), pro řezání laserem, plazmou a vodním paprskem. Součástí jsou dvě věci, které obdélníkové API vyjádřit neumí: vyloučené zóny po jednotlivých deskách (stock[].exclusions — vada, otisk upínky, předtištěná plocha; zóna s quality 0 je zakázaná oblast pro jakýkoli díl) a otvory podle skutečného tvaru. material partition a průchod meta fungují jako všude jinde. Příklad níže je jedno skutečně zachycené volání — osm dílů na jedné desce s vyloučeným poškozeným rohem.

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

partsarrayOne or more NestPart (see below). Required.
stockarrayOne or more NestStock sheet types (see below). Required.
optionsobjectSolve options (see below). Optional.
enginestring"lbf" (default, single-pass, instant) or "sparrow" (advertised for a future higher-density build; currently served by lbf with a warning).
includestring[]Additive inline export of the ACHIEVED nest: "svg" a self-contained styled drawing (one titled band per sheet, parts as filled polygons with holes, exclusion zones hatched), "dxf" an R12/AC1009 document on layers STOCK/PARTS/HOLES/ZONES/LABELS. Unit-agnostic, exactly like the request coordinates. The geometry in sheets[] is returned either way.

parts[] (NestPart)

polygonnumber[][]The part outline: an ordered ring of ≥3 [x, y] vertices. Given closed (first == last) or open; winding order is not required — it is oriented internally. Required UNLESS the row carries source.
sourceobjectOPEN-262 — read the outline out of an SVG or DXF FILE instead of listing coordinates (fields below). EXACTLY ONE of polygon / source: both, or neither, is a 400.
holesnumber[][][]Optional interior holes — a part with a cut-out. Each hole is a ring like polygon. NOT accepted alongside source: the file already carries its own holes, and two sources of truth for one geometry is not a thing we resolve silently.
qtyintegerCopies to place. Default 1.
allowedRotationsnumber[] | "continuous"Allowed rotations in DEGREES (e.g. [0,90,180,270]). Omit or "continuous" for free rotation.
minQualityintegerThe 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: wins sheet space when stock is capped (options.respectStock).
materialstringOPEN-256 — a part of material X nests only on material-X sheets; the job is partitioned by material.
namestringOptional label, echoed on every placement. Defaults to "Part <1-based row index>".
metaobjectOPEN-224 — opaque JSON (your ERP ids), echoed verbatim on every placed copy. Never affects the layout.

stock[] (NestStock)

w, hnumberRectangular sheet size. Give w & h OR polygon, not both.
polygonnumber[][]Arbitrary sheet outline (an off-cut remnant, a non-rectangular board); overrides w/h.
qtyintegerDefault 1. A HARD cap only when options.respectStock is true.
pricenumberPer-sheet price, for cost mode + totalPrice.
exclusionsobject[]OPEN-233 — per-sheet excluded / inferior zones. Each: { polygon: number[][], quality?: integer }. quality 0 (the default) = a hard no-go region for any part; a higher quality only excludes parts whose minQuality demands at least that. This is what the rectangular API cannot express.
materialstringOPEN-256 — this sheet serves only material-matching parts.
metaobjectOPEN-224 — echoed on every sheet cut from this stock row.

options (NestOptions)

minSeparationnumberMinimum clearance between parts and between a part and any hazard (sheet edge / exclusion zone). Use for kerf / beam / torch width. Default 0.
seedintegerDeterminism: a fixed seed → a reproducible layout. Omit and the server pins a fixed default so the response stays reproducible.
minimizeCostbooleanRank plans by total sheet price rather than sheet count.
respectStockbooleanTreat each stock qty as a hard cap.
simplifyTolerancenumberPolygon simplification tolerance (max area deviation as a fraction). Speeds up dense DXF outlines with hundreds of vertices. 0 disables.
timeBudgetMsintegerWall-clock budget for the metaheuristic (engine "sparrow"). Ignored by "lbf" (single-pass).

Nest — odpověď

{
  "engine": "lbf",
  "engineVersion": "1.0.0+nest-d4046d5-07546022",
  "contractVersion": "1",
  "deterministic": true,
  "sheets": [
    {
      "w": 2440,
      "h": 1220,
      "price": 12.5,
      "density": 0.1217,
      "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 },
  "unplaced": [],
  "warnings": [],
  "timing": { "solveMs": 44.19 }
}

Každá položka v sheets je jedna použitá deska; umístěný díl nese tuhou transformaci (rotation ve stupních, pak posun x/y), NIKOLI znovu vygenerovaný polygon — otočte svůj vstupní obrys o rotation kolem jeho počátku a přičtěte (x, y), čímž umístění přesně zrekonstruujete. rotation může být záporná; rekonstrukce je přesná bez ohledu na znaménko. ⚠️ density je plocha umístěného POLYGONU vůči ploše použité desky — poctivé zaplnění, kde se konkávní kapsy počítají jako prázdné — a NENÍ srovnatelná s yieldPct obdélníkového packeru (který počítá každý opsaný obdélník jako plný, takže u horšího výsledku vychází vyšší); srovnatelnou metrikou mezi oběma je sheetCount na týchž dílech. Rozvržení je deterministické: nastavte options.seed, chcete-li ho zopakovat. exclusions se vrací u každé desky kvůli vykreslení.

POST /v1/optimize/nest — top level

enginestringWhich nesting engine ran: "lbf" or "sparrow".
engineVersionstringThe nesting engine's algorithm identity (jagua-rs revision + build hash). 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.
deterministicbooleanAlways true — guaranteed by the pinned seed.
sheetsarrayOne entry per used sheet.
metricsobjectJob totals (see below).
unplacedarrayDemand that could not be placed — a plan-plus-warning, not an error.
warningsstring[]e.g. an engine substitution ("sparrow" served by "lbf"), unplaced parts, or a material with no matching stock.
materialsarrayOPEN-256 per-material rollup — present only when parts/stock carry material.
unmatchedMaterialsarrayParts whose material has no matching stock — present only when it happens.
importedobjectOPEN-262 — what the request’s source files contributed (fields above). ABSENT for a coordinates-only job, which is what keeps such a request byte-identical to before the feature existed.
svg, dxfstringThe achieved nest as an inline drawing — present ONLY when include contains that token. svg is self-contained (no external refs); dxf is R12/AC1009.
timingobject{ solveMs: number } — the solve time; environment-dependent.

sheets[] (nest)

w, hnumberPresent for rectangular sheets.
polygonnumber[][]Present for arbitrary-outline sheets instead of w/h.
pricenumber | nullThe stock row's price, or null.
densitynumberThis sheet's fill = placed polygon area / sheet area.
partsarrayPlacements on this sheet (see below).
exclusionsobject[]The zones that applied to this sheet, echoed for rendering.
materialstringPresent when the sheet carried a material (OPEN-256).
metaobjectEchoed from the stock row's meta (OPEN-224).

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

namestringThe requested name, or the generated default.
sheetinteger0-based index into sheets.
x, ynumberTranslation, applied AFTER rotation about the part's origin.
rotationnumber⚠️ Degrees, applied about the part's own origin FIRST. A placed part carries this rigid transform, NOT a re-emitted polygon: rotate your input outline by rotation, then add (x, y) to reconstruct the placement exactly. May be negative; the reconstruction is exact regardless of sign.
materialstringThe material this copy nested from (OPEN-256).
metaobjectThe part's opaque passthrough (OPEN-224).

metrics (nest)

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.
placedintegerPart copies placed, after qty expansion.
totalintegerPart copies requested, after qty expansion.
totalPricenumberSum of used sheet prices.

unplaced[] (nest)

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

rods[]

lengthnumberFULL bar length as supplied in stock.
pricenumber | nullPrice of the stock row, or null.
remainingnumberUSABLE off-cut left on this bar, 3 decimals — the kerf of the cut that frees it from the last piece is already deducted (OPEN-257), so it is the reclaimable length, not the raw gap. Reported even when below minOffcut.
partsarrayPlacements, in cutting order along the bar.
offcutsnumber[]At most one entry: [remaining] (the kerf-corrected usable off-cut, OPEN-257) when it is > 0 and ≥ minOffcut, else []. Absent when excluded via include.
metaobjectPresent only when the stock row carried meta — echoed from stock[].meta (OPEN-224). Applies to 1D and wood rods.

rods[].parts[]

namestringThe requested name, or the generated default.
posnumberOffset of the part’s NEAR end from the bar end that trim.start trims.
lengthnumberThe part length, as requested.
metaobjectPresent only when the part carried meta — echoed from parts[].meta (OPEN-224).

metrics (1D)

rodCountintegerBars used. Equals rods.length.
yieldPctnumberPlaced length ÷ total FULL bar length × 100, 2 decimals. Trim and kerf count as waste.
placedintegerPieces placed, after qty expansion.
totalintegerPieces requested, after qty expansion.
cutsintegerTotal crosscuts across all used bars — one per placed piece.
totalPricenumberSum of used bar prices, 2 decimals.
toleranceAcceptedCountintegerPieces that fitted only because options.tolerance allowed an overshoot.

unplaced[] (1D)

namestringThe part name.
lengthnumberAs requested.
qtyintegerHow many could not be placed.

Díly ze souboru (SVG · DXF)

Díl nemusí přijít jako souřadnice. Vložte dokument SVG nebo DXF do parts[].source a server z něj vytáhne obrys — i s jeho otvory — stejným čtečem, jaký používá aplikace CutOptim, když na její režim Nesting přetáhnete výkres. Soubor nahrazuje POUZE geometrii: qty, material, allowedRotations, minQuality, priority a meta se chovají přesně jako u dílu polygon, takže knihovna dílů, která už existuje jako CAD soubory, nevyžaduje váš vlastní rozklad křivek a oblouků. Jedno source popisuje JEDEN díl; výkres s několika samostatnými součástmi vrátí 400 a odkáže na importní endpoint níže. Odpověď pak nese blok imported: kolik řádků přišlo ze souboru, kolik vrcholů vytvořily a jaké jednotky ty soubory deklarovaly — hlášeno, nikdy použito, protože toto API nic nepřepočítává.

Nic se neukládá. Bajty existují pouze jako tělo požadavku, zpracují se v paměti a zmizí, jakmile je odpověď zapsána: žádný disk, žádná databáze, žádný dočasný soubor, žádný řádek v logu. Poté není co mazat a nic nezůstává — stejná bezstavovost, jakou drží každý jiný endpoint.

POST /v1/optimize/nest

{
  "parts": [
    {
      "source": {
        "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 200\">\n  <path d=\"M0 0 H120 V80 H0 Z\"/>\n  <path d=\"M40 30 H80 V50 H40 Z\"/>\n</svg>",
        "filename": "washer-plate.svg"
      },
      "qty": 4,
      "meta": { "erpId": "ART-8891" }
    }
  ],
  "stock": [{ "w": 2440, "h": 1220 }]
}

Názvy polí a jejich typy jsou součástí kontraktu, takže tabulky níže zůstávají ve všech jazycích v angličtině — přeložený název pole by dokumentoval API, které neexistuje.

{
  "imported": { "parts": 1, "vertices": 8, "units": [] },
  "parts_after_import": [
    {
      "name": "washer-plate",
      "polygon": [[0, 0], [120, 0], [120, 80], [0, 80]],
      "holes": [[[40, 30], [80, 30], [80, 50], [40, 50]]],
      "qty": 4,
      "meta": { "erpId": "ART-8891" }
    }
  ]
}

parts[].source — geometry from an SVG / DXF file

contentstringThe file TEXT — not base64, not a URL, not a multipart upload. Both formats are text documents, so they travel inside the JSON body like any other field. At most 4,000,000 characters per file and 8,000,000 across one request; over either is a 413.
format"svg" | "dxf"Omit and the server sniffs the content (an <svg tag ⇒ svg, otherwise dxf). An explicit value ALWAYS wins — including over a filename whose extension disagrees, which is the case worth setting it for.
filenamestringUsed for format detection AND as the part name when the row has no name of its own (extension stripped). It never touches a filesystem — there is no file on our side to name.
flattenTolerancenumberCurve and arc flattening tolerance, in the FILE’s own coordinate unit. Default 0.2. Larger = fewer vertices; this is the lever when a dense outline trips the 2,000-vertex per-ring cap.

imported — what the files contributed (response, absent without a source)

partsintegerPart ROWS whose geometry came from a file.
verticesintegerTotal vertices those files produced after flattening, outlines and holes together — the number to watch against the per-ring cap.
unitsstring[]⚠️ Units the files DECLARED (a DXF $INSUNITS), not units we applied. Empty when none declared one. More than one entry also raises a warning: mixing a millimetre drawing with an inch one produces a plan that validates and is physically wrong, and the server must not "fix" that by converting — no field in this API asserts a unit.

POST /v1/import/nest — jeden soubor, všechny obrysy

Když jeden výkres obsahuje několik různých dílů, nejprve jej importujte: tento endpoint vrátí každý uzavřený obrys, který obsahuje, od největšího, přesně v podobě, jakou očekává řádek parts[]. Vložte ty, které potřebujete, doplňte vlastní qty a material a to pošlete na /v1/optimize/nest. Je to také způsob, jak zjistit, co v souboru je, dřív než na něj vydáte výpočet. Vyžaduje klíč — rozklad libovolné geometrie je skutečná práce procesoru a anonymní procesor je špatný obchod — ale nic nerezervuje: vaše kvóta zůstává nedotčená a nevracejí se hlavičky rate limit, přesně jako při dotazování úlohy.

POST /v1/import/nest
Authorization: Bearer co_live_…

{
  "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 200\">\n  <path d=\"M0 0 H120 V80 H0 Z\"/>\n  <path d=\"M40 30 H80 V50 H40 Z\"/>\n</svg>",
  "filename": "washer-plate.svg"
}
{
  "format": "svg",
  "parts": [
    {
      "name": "washer-plate",
      "polygon": [[0, 0], [120, 0], [120, 80], [0, 80]],
      "holes": [[[40, 30], [80, 30], [80, 50], [40, 50]]]
    }
  ],
  "vertices": 8,
  "warnings": [],
  "engineEnabled": true,
  "contractVersion": "1"
}

POST /v1/import/nest — request

contentstringThe file text. Required. Same 4,000,000-character cap as parts[].source.
format"svg" | "dxf"Omit to sniff; an explicit value wins over the filename extension.
filenamestringUsed for detection and to NAME the results: a file with one outline keeps the bare name, several are numbered "<name> 1", "<name> 2", …
flattenTolerancenumberAs on parts[].source. Default 0.2.

POST /v1/import/nest — response

format"svg" | "dxf"The parser that actually ran — the useful bit when you let the server sniff.
parts[]{ name, polygon, holes? }One entry per closed outline, LARGEST FIRST. Each is already in the shape a parts[] row wants: paste it in and add your own qty / material / allowedRotations. Deterministic — the same bytes always give the same vertices in the same order.
verticesintegerTotal vertices after flattening. Compare it against the 2,000-per-ring cap before you build a large request.
sourceUnit"mm" | "in"A DXF $INSUNITS declaration, when the file carries one. REPORTED, NEVER APPLIED. Absent for SVG (the format has no unit) and for a DXF that declares none.
warningsstring[]What the parser could not honour — an SVG transform= attribute (CAD part exports are flat, so we do not apply them), or geometry that never closed into a contour. Empty means the file was read whole.
engineEnabledbooleanWhether THIS deployment can also SOLVE a nest. Importing is pure parsing and works everywhere; where the nesting engine is not built in, /v1/optimize/nest answers 503 and this flag says so up front. Same field /v1/validate/nest carries.
contractVersionstringNest contract version. Currently "1".

Jádra

  • heuristic (výchozí) — gilotinový rozmisťovací algoritmus s více strategiemi. Nejvyšší využití, každé rozvržení je řezatelné na pile, vždy plný cutPlan.
  • balanced — rozmisťovací algoritmus MaxRects s volným nestingem. Na velkých zakázkách výrazně rychlejší (naměřeno ~25× při 2 000 dílech) za cenu malé ztráty využití a jeho rozvržení často nejsou gilotinová (guillotineValid: false, cutPlan: null). Nemodeluje tolerance, minimizeCost, grainGroup, maxCutStages ani minimizeRotations — když některou nastavíte, upozornění vám řekne, že byla ignorována.
  • max — asynchronní vrstva s prohledáváním stromu (jen 2D): dosahuje prokázaného optima u mnohem více zakázek za cenu sekund až minuty na jeden výpočet. Stále deterministické a gilotinové. Plán nevrací přímo — viz Asynchronní úlohy níže. Modeluje JEDINÝ formát materiálu v plné velikosti desky, v neomezeném množství a s pevným 3fázovým gilotinovým vzorem: druhý řádek materiálu, trim, respectStock, material nebo grainGroup jsou odmítnuty kódem 400 ještě před rezervací volání; tolerance, minimizeCost, maxCutStages, minimizeRotations, firstCut a effort projdou, ale jsou ignorovány s varováním, a výsledek max nehlásí žádné odřezky. Takové zakázky posílejte do jádra heuristic.

Asynchronní úlohy (engine = max)

Výpočet max trvá sekundy až minutu, takže POST /v1/optimize/2d s engine:"max" nevrací plán — vrací 202 Accepted s jobId a volání se započítá při odeslání. Dotazujte se na GET /v1/jobs/{id}, dokud status není "succeeded" (result nese stejnou 2D odpověď jako synchronní výpočet) nebo "failed" (error nese zprávu). Dotazování nespotřebovává kvótu; vidíte jen své vlastní úlohy. Tam, kde tato vrstva na dané instalaci není zapnutá, selže engine:"max" s 503 (fail closed).

fieldtypemeaning
jobIdstringThe 202 body's id. Poll GET /v1/jobs/{id}.
statusstringqueuedrunningsucceeded | failed.
mode · enginestringAlways "2d" and "max".
pollAfterMsinteger202 only — suggested delay before the first poll.
resultobjectPresent once succeeded — the same shape as a synchronous 2D response.
errorstringPresent once failed — the reason.
quotaobject202 only — reserved, used and limit: the one call metered at submission, and where the ACCOUNT stands this month.
createdAtstringPoll only — when the job was submitted (ISO-8601).
finishedAtstring | nullPoll only — when the worker finished; null while queued or running.

Determinismus a verzování

Každá odpověď nese engineVersion. Algoritmus je deterministický, takže jeho zlepšení změní výstup pro stejný vstup — a to je pro toho, kdo cachuje, nekompatibilní změna. Chování si zafixujete tím, že budete engine posílat explicitně a sledovat engineVersion; verze v cestě /v1/ se mění jen tehdy, když se změní podoba odpovědi.

Každé jádro se verzuje samostatně, takže změna jednoho nikdy neposune verzi druhého.

Chyby

400invalid_requestChyba schématu. details.path ukazuje na problematické pole.
401unauthorizedChybějící nebo neznámý API klíč.
402quota_exceededVyčerpaná měsíční kvóta. Retry-After udává počet sekund do začátku nového měsíce.
403key_revokedKlíč existuje, ale nesmí být použit: byl zneplatněn, nebo předplatné Engine API pro tento účet už není aktivní. Pole message řekne, o který případ jde.
404not_foundTaková cesta neexistuje — a totéž dostanete, když je cesta správná, ale metoda špatná.
413too_largeVstup překračuje limit (viz Limity).
429busyMomentálně plná kapacita. Retry-After v sekundách — do vaší kvóty se to nikdy nepočítá.
500internalNeočekávaná chyba, nebo je nedostupný autentizační backend (požadavky se odmítají — fail closed).
503service_unavailablePožadované jádro nelze právě teď obsloužit — nesting jádro nebo asynchronní jádro max. U max jsou dvě příčiny a pole message řekne která: vrstva není v tomto nasazení zabudovaná, nebo zabudovaná je, ale worker, který úlohy řeší, neodpovídá. Fail-closed před rezervací volání, takže vás to nikdy nic nestojí.
504solve_timeoutVýpočet překročil svůj tvrdý časový limit. Na obdélníkových cestách jej vynucuje proxy; u /v1/optimize/nest vynucuje jádro vlastní, kratší rozpočet a odpovídá kódem solve_timeout v běžné obálce.

Tělo chyby

Každá chyba, kterou vytvoří samotné jádro, používá stejnou obálku. Rozhodujte se podle error, což je stabilní kód; nikdy podle message, jehož formulace se mezi vydáními může změnit. details je přítomné u invalid_request, kde path pojmenovává problematické pole, a u too_large, kde max a got udávají limit a to, co jste poslali.

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{
  "error": "invalid_request",
  "message": "parts[0].h: must be greater than 0",
  "details": { "path": "parts[0].h" }
}
HTTP/1.1 402 Payment Required
Retry-After: 41400
Content-Type: application/json; charset=utf-8

{ "error": "quota_exceeded", "message": "monthly quota exhausted" }
  • 402 i 429 nesou hlavičku Retry-After v sekundách. U 402 odpočítává čas do resetu kvóty v 00:00 UTC prvního dne následujícího měsíce; u 429 jde o krátké počkání a 429 nikdy nespotřebuje kvótu — rezervované volání se vrací zpět.
  • Chyba směrování odpovídá kódem not_found, který záměrně není v seznamu výše, protože jej vytváří router, a ne kontrakt API. Když je cesta správná, ale metoda špatná, dostanete 404, a ne 405: všechny čtyři optimalizační endpointy přijímají jen POST.
  • Na obdélníkových cestách přichází 504 z reverzní proxy, ne z jádra, takže jeho tělo patří proxy, a ne této JSON obálce; v rámci vstupních limitů níže by nemělo být možné jej vůbec dostat. Výjimkou je /v1/optimize/nest: ten výpočet je podproces s vlastním rozpočtem, záměrně drženým pod limitem proxy — vypršení času tam tedy používá tuto obálku, s kódem solve_timeout.

Hlavičky rate limitu

Úspěšné volání optimalizace nese X-RateLimit-Limit (měsíční strop ÚČTU — všechny klíče účtu sdílejí jeden) a X-RateLimit-Remaining (kolik volání účtu v tomto měsíci zbývá po tomto). Posílají je pouze optimalizační endpointy: počítadlo se rezervuje jako součást autorizace výpočtu, takže /v1/usage a /v1/health nemají co hlásit.

X-RateLimit-LimitintegerThe ACCOUNT’s monthly quota — the same number GET /v1/usage returns as limit, not a per-key cap (049).
X-RateLimit-RemainingintegerCalls left this month on the ACCOUNT, after this one.
Retry-AfterintegerSeconds to wait. Sent with 402 and 429 only.

GET /v1/usage

{
  "plan": "studio",
  "used": 137,
  "limit": 10000,
  "remaining": 9863,
  "periodEnd": "2026-08-01",
  "keyPrefix": "co_live_ab12",
  "contractVersion": "1"
}

Jen pro čtení: nespotřebuje volání a neposílá hlavičky rate limitu. ⚠️ used a limit popisují ÚČET, ne klíč, kterým jste volali: každý aktivní klíč účtu čerpá z jedné společné kvóty, takže více klíčů neznamená více kvóty. used počítá probíhající kalendářní měsíc UTC přes všechny, remaining je limit minus used a nikdy neklesne pod nulu, periodEnd je den vynulování jako prosté datum YYYY-MM-DD a keyPrefix je nerizikový zobrazovací prefix použitého klíče. Samotný klíč nevrací žádný endpoint — ukládá se jen jeho hash, ztracený klíč se tedy nahrazuje, ne obnovuje.

planstringTier slug frozen onto the key when it was created.
usedintegerCalls counted in the current UTC calendar month across EVERY key on the ACCOUNT — revoked keys included, so revoking a key cannot un-spend what it spent (053).
limitintegerThe ACCOUNT’s cap: the largest monthly quota frozen onto any of its ACTIVE keys (049). Not a per-key allowance — more keys do not add quota.
remainingintegerlimit − used, never negative.
periodEndstringReset day as YYYY-MM-DD — a date, not a timestamp.
keyPrefixstringNon-secret display prefix of the calling key.
contractVersionstringShape version. Currently "1".

GET /v1/health

{
  "status": "healthy",
  "service": "cutoptim-engine",
  "contractVersion": "1",
  "engineVersion": "1.0.0+10e0c941",
  "engines": ["heuristic", "balanced"],
  "modes": ["2d", "1d", "wood", "nest"],
  "nestEngines": ["lbf"],
  "maxEngines": ["max"],
  "uptimeSec": 16
}

Bez klíče, bez kvóty, bez databáze. Záměrně se nedotýká ničeho stavového, takže výpadek úložiště klíčů nemůže službu ukázat orchestrátoru jako mrtvou. engines vypisuje id, která tato instalace přijímá v engine, a engineVersion je verze výchozího jádra.

statusstringAlways "healthy" when the process answers.
servicestringAlways "cutoptim-engine".
contractVersionstringShape version. Currently "1".
engineVersionstringThe DEFAULT engine’s version, not a per-engine list.
enginesstring[]SYNCHRONOUS engine ids this deployment accepts in engine — ["heuristic","balanced"]. The async max tier is reported separately in maxEngines, never here.
modesstring[]Optimize paths this deployment serves: "2d", "1d", "wood", plus "nest" only where the nesting engine is built in. Endpoint discovery without reading this page.
nestEnginesstring[]Nesting engine ids this deployment can serve — ["lbf"] on the production API, [] where the Rust nesting stage is not built in.
maxEnginesstring[]The async tree-search tier — ["max"] where it is enabled, [] otherwise. Health never advertises a capability it cannot serve.
uptimeSecintegerWhole seconds since process start.

Limity

  • 2,000 dílů na požadavek (celkové množství, po rozepsání qty)
  • 50 řádků materiálu · tělo požadavku až 1 MB
  • 10 aktivních klíčů na účet — sdílejí JEDNU měsíční kvótu: klíče oddělují prostředí a integrace, kvótu nezvyšují
  • 10 MB těla požadavku na těch dvou nest cestách, které mohou nést výkres (/v1/optimize/nest a /v1/import/nest); jeden soubor source nejvýše 4 000 000 znaků, 8 000 000 na požadavek
  • souběžnost je omezena na straně serveru — burst dostane 429, nikdy pomalou frontu. Endpointy validate bez klíče mají navíc strop na adresu (429 s Retry-After); s klíčem k takovému škrcení nikdy nedojde. Účet může mít současně 5 úloh max ve stavu queued/running.

Specifikace OpenAPI

Strojově čitelný dokument OpenAPI 3.1 popisuje všech dvanáct endpointů, každé tělo požadavku, každou podobu odpovědi a každou chybu. Nasměrujte na něj svůj generátor klienta místo toho, abyste přepisovali tuto stránku. Samotný dokument je jen v angličtině: skládá se z tokenů kontraktu a OpenAPI žádný lokalizační mechanismus nemá.

curl -s https://cutoptim.com/engine/openapi.json > cutoptim-engine.json

Otevřít dokument OpenAPI 3.1 →

Materiál ke stažení
Engine API one-pager

A two-page summary of the Engine API — the four modes (2D, 1D, wood and true-shape nest), a request and response, determinism and pricing. Print-ready, with a QR back to the docs.

PDF2 pagesFree
Stáhnout PDF