Skip to main content
← API reference

Timber with cross-sections

The length problem with a cross-section: a 50×150 part only comes out of 50×150 stock.

Wood — cross-section

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 }
}
Uses a public demo key — rate-limited, small jobs only, never billed.

Timber has an identity a plain bar does not: a 50×150 part cannot come out of 50×100 stock, however much length is left. Parts and stock therefore carry sw and sh, the two cross-section sides, in either order — 50×100 and 100×50 are the same bar turned over and are matched as one section. The job is split by cross-section, each section is matched to its own stock and solved on its own, and one call returns the whole thing.

Parts and stock also accept an optional material tag (stock also priority): with it, an oak 50×100 and a pine 50×100 become two separate sections, and each section carries its material. Options are the same as 1D.

Wood — response

{
  "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 replaces rods at the top level: each entry is one cross-section with its own rods (identical in shape to 1D) and its own metrics, so the per-material figures are there without recomputing them. unmatched has no 1D equivalent — it is demand whose cross-section you supplied no stock for at all, which is a different problem from unplaced (parts that had stock and did not fit) and has a different fix, so the two are never mixed. metrics.total counts every piece you asked for, unmatched ones included.

In the cut plan each step also names its section, and sheet is the rod index WITHIN that section rather than a job-wide counter.

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

FieldTypeDescription
sectionsWoodSection[]Replaces rods at the top level: one entry per cross-section that had matching stock, in request order — each matched to its own stock and solved on its own.
unmatchedWoodUnmatched[]Demand for a cross-section you supplied NO stock for. Deliberately separate from unplaced: the fix is to declare or buy the material, not to add length or relax a limit.
metricsMetricsWoodJob-wide totals across every section.
cutPlanWoodCutStep[] | nullOne step per placed piece across all sections — as 1D, except every step also carries section, and sheet is the rod index WITHIN that section, not a job-wide counter. ABSENT when include excludes cutPlan. Never null in practice — a linear plan is always producible.
unplacedUnplaced1D[]Did-not-fit parts aggregated across sections that DID have stock. Parts with no stock at all are in unmatched instead.
csvstringInline CSV cut list. Present ONLY when include contains csv.
dxfstringInline DXF (R12/AC1009). Present ONLY when include contains dxf. (svg is 2D only — a warning is returned instead.)

sections[]

FieldTypeDescription
sectionstringNormalised key <short>x<long>, e.g. 50x100 — short side first, so 50×100 and 100×50 are one section.
swnumberShort side of the cross-section (normalised).
shnumberLong side of the cross-section (normalised).
stockNamestring | nullLabel of the stock row matched to this section, or null if none was given.
materialstringThe section's material. Two materials at one cross-section are TWO sections. Absent when the job carries no material.
rodsRod[]Identical shape to a 1D rod.
metricsMetricsSectionOne cross-section's own totals — no need to re-derive them from rods.
metrics.rodCountintegerBars used in this section.
metrics.yieldPctnumberPlaced length ÷ full bar length × 100 FOR THIS SECTION, 2 decimals.
metrics.placedintegerPieces of this section placed, after qty expansion.
metrics.totalintegerPieces of this section requested, after qty expansion.
metrics.cutsintegerCrosscuts in this section — one per placed piece.
metrics.totalPricenumberSum of this section's used bar prices, 2 decimals.
unplacedUnplaced1D[]Parts of THIS section that had stock but did not fit in it.

metrics (wood)

FieldTypeDescription
sectionCountintegerCross-sections that were solved. Equals sections.length; excludes unmatched sections.
rodCountintegerBars used across every section.
yieldPctnumberJob-wide placed length ÷ full bar length × 100, 2 decimals. Computed over the bars actually used, so an unmatched section neither helps nor hurts it.
placedintegerPieces placed across every section.
totalintegerEvery piece requested — INCLUDING those in unmatched sections.
cutsintegerCrosscuts across every used bar.
totalPricenumberSum of the used bars’ prices, 2 decimals.
toleranceAcceptedCountintegerHow many pieces fitted only because options.tolerance allowed an overshoot.

unmatched[]

FieldTypeDescription
sectionstringThe cross-section key nothing in stock matched.
sw, shnumberThat cross-section's two sides (normalised: short, long).
materialstringSet when the section exists but only in a DIFFERENT material.
partsUnplaced1D[]The demand rows in that section, in the 1D unplaced shape: name, length, qty.
qtyintegerTotal pieces in this section, summed over parts.