{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/smartcut-dev/json-schema/smartcut-dev-calculate-request-schema.json",
  "title": "CalculateRequest",
  "description": "Calculation request",
  "x-generated": "2026-10-31",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/_original/smartcut-dev-openapi-openapi.json#/components/schemas/CalculateRequest",
  "type": "object",
  "properties": {
    "saw": {
      "description": "Saw configuration. Either a single saw — every stock is cut on it, exactly as before — or a NON-EMPTY ARRAY of identified saws for a MIXED job. Array form: the FIRST element is the job's default saw, and `materials` bind stock/parts to the others via `sawId`. Which settings apply to which saw, and what the platform does when two of them disagree, is one published registry — browse it (and try your own saw against it) at https://smartcut.dev/api-docs/saw-settings; anything it resolves comes back on the response `warnings`.",
      "oneOf": [
        {
          "$ref": "#/$defs/Saw"
        },
        {
          "type": "array",
          "minItems": 1,
          "items": {
            "$ref": "#/$defs/IdentifiedSaw"
          }
        }
      ]
    },
    "stock": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Stock"
      },
      "description": "Stock items"
    },
    "parts": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Part"
      },
      "description": "Parts to cut"
    },
    "materials": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Material"
      },
      "description": "Materials with saw associations — the groups a MIXED job is partitioned and optimised by. Stock/parts attach to a material via their `materialId` (or by material name + thickness); each material's group runs on its `sawId` / inline `saw`, or on the job's default saw when neither is set. Omit for a classic single-saw job."
    },
    "groups": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/Group"
      },
      "description": "User-defined part groups with XY positions"
    },
    "model": {
      "type": "string",
      "enum": [
        "rectangle-guillotine-fast",
        "rectangle-guillotine-max-1",
        "rectangle-guillotine-max-2",
        "rectangle-guillotine-max-3",
        "irregular-nesting-fast",
        "irregular-nesting-max-1",
        "irregular-nesting-max-2"
      ],
      "default": "rectangle-guillotine-fast",
      "description": "Optimisation model. Optional; defaults to `rectangle-guillotine-fast`.\n\n| Model | Budget | Use it when |\n| --- | --- | --- |\n| `rectangle-guillotine-fast` | Seconds | Default. Interactive use and live quoting |\n| `rectangle-guillotine-max-1` | ~60s | A cheap second look (beam/guillotine-only, needs a recognised saw profile on beam, uncapped jobs only (no maxPhase from the request or the saw profile), ≤500 parts) |\n| `rectangle-guillotine-max-2` | ~150s | The Max to reach for first (beam/guillotine-only, needs a recognised saw profile on beam, uncapped jobs only (no maxPhase from the request or the saw profile), ≤500 parts) |\n| `rectangle-guillotine-max-3` | ~420s | Batch or overnight, where board cost dominates (beam/guillotine-only, needs a recognised saw profile on beam, uncapped jobs only (no maxPhase from the request or the saw profile), ≤500 parts) |\n| `irregular-nesting-fast` | Seconds | Default for `nesting` jobs. Interactive use and live quoting |\n| `irregular-nesting-max-1` | +5s search | The nesting Max to reach for first — most of the gain; adds ~5s of search to the base solve (nesting-only) |\n| `irregular-nesting-max-2` | +30s search | Nesting jobs above ~50 parts, where material cost dominates (nesting-only) |\n\n**Max** spends up to the budget above searching for a better layout, and can save boards on larger jobs. Compute time is billed per millisecond, so a Max run costs in line with the time it uses.\n\n**Nesting (`saw.cutType: \"nesting\"`) has its own tiers**, which pack parts tighter: `irregular-nesting-max-1` adds about 5s of search and `irregular-nesting-max-2` about 30s, best on jobs above ~50 parts. The budget is extra search on top of the base solve, so a large job takes one full solve plus roughly the budget.\n\nEach Max tier has the requirements shown in its row. A request that doesn't meet them runs `rectangle-guillotine-fast` instead, and naming a tier for the wrong cut type is never an error. Full catalogue: `GET /v3/spec`.\n\n**On a beam saw, Max needs a recognised saw profile AND an uncapped job.** A beam job with no recognised profile (`sawProfileId`, or `saw.vendor` + `saw.model`) runs `rectangle-guillotine-fast` (\"Max needs a recognised beam saw profile …\"). Any phase cap — `saw.guillotineOptions.maxPhase` above 0, or the default a saw profile sets (the Mayer and Biesse profiles cap at 3) — also runs `rectangle-guillotine-fast` (\"MAX needs an uncapped job; this saw/request sets maxPhase N\"). The result's `modelDowngrade.reason` names which. Sending `maxPhase: 0` does not remove a saw profile's cap: 0 means \"not set\", and the profile fills it."
    },
    "webhook": {
      "type": "string",
      "format": "uri",
      "description": "Webhook URL for async result delivery"
    },
    "labels": {
      "type": "object",
      "description": "Typed saw label-printer fields: `parts[].labels` + top-level `labels.customer`. Emitted by PTX and Mayer .MAY (part-level fields); `labels.customer` is PTX-only; Biesse XML to follow. Schema may change; see `/v3/export/ptx` and `/v3/export/mayer`.",
      "properties": {
        "customer": {
          "type": "string",
          "maxLength": 100,
          "description": "Customer name printed on saw labels (JOBS.CUSTOMER in PTX)"
        }
      }
    }
  },
  "required": [
    "saw",
    "stock",
    "parts"
  ],
  "$defs": {
    "Extras": {
      "type": "object",
      "description": "Extras container (V3 format)",
      "properties": {
        "banding": {
          "type": "object",
          "properties": {
            "sides": {
              "$ref": "#/$defs/SideValues"
            }
          },
          "description": "Edge banding"
        },
        "finish": {
          "type": "object",
          "properties": {
            "faces": {
              "type": "object",
              "properties": {
                "a": {
                  "type": "string",
                  "description": "Front face"
                },
                "b": {
                  "type": "string",
                  "description": "Back face"
                }
              }
            },
            "sides": {
              "$ref": "#/$defs/SideValues"
            }
          },
          "description": "Surface finish"
        },
        "planing": {
          "type": "object",
          "properties": {
            "sides": {
              "$ref": "#/$defs/SideValues"
            },
            "faces": {
              "type": "object",
              "properties": {
                "a": {
                  "type": "boolean"
                },
                "b": {
                  "type": "boolean"
                }
              }
            }
          },
          "description": "Planing operations"
        },
        "imageUpload": {
          "type": "object",
          "description": "Image upload data"
        }
      }
    },
    "Group": {
      "type": "object",
      "description": "User-defined part group with XY coordinate-based positioning. The group is placed as one rigid unit at the authored positions; it never rotates as a whole. Member orientation is NOT part of the group: it defers entirely to each part's `orientationLock` — a position has no rotation of its own, and `rot` on results is derived from the final dimensions.",
      "properties": {
        "positions": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/GroupPosition"
          },
          "minItems": 2,
          "description": "Part positions within the group, authored against each part's LOCKED footprint: a part with `orientationLock: \"w\"` occupies its rotated dimensions (short side along x), a part locked \"l\" — or with no lock — occupies its landscape dimensions (long side along x). Include the kerf in the pitch when parts should touch (e.g. 200-wide members with a 3.2 blade sit at x = 0, 203.2, 406.4). Validation checks overlaps in this same frame and names the offending pair."
        },
        "q": {
          "type": "number",
          "minimum": 1,
          "description": "Quantity of groups"
        }
      },
      "required": [
        "positions",
        "q"
      ]
    },
    "GroupPosition": {
      "type": "object",
      "description": "Position of a part within a user group. Coordinates place the part's locked footprint (see the positions description on the group) — there is no per-position rotation field by design: orientation belongs to the part via `orientationLock`.",
      "properties": {
        "part": {
          "type": "number",
          "minimum": 0,
          "description": "Index of the part in the parts array"
        },
        "x": {
          "type": "number",
          "minimum": 0,
          "description": "X position within group (0 = left edge, increases rightward)"
        },
        "y": {
          "type": "number",
          "minimum": 0,
          "description": "Y position within group (0 = bottom edge, increases upward)"
        }
      },
      "required": [
        "part",
        "x",
        "y"
      ]
    },
    "IdentifiedSaw": {
      "description": "A saw with an id — the array form of `saw` and the `ResultResponse.saws` echo. Materials reference it via `sawId`.",
      "allOf": [
        {
          "$ref": "#/$defs/Saw"
        },
        {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "minLength": 1,
              "description": "Identifier materials reference via `sawId`. The FIRST element of the `saw` array is the job's default saw."
            }
          },
          "required": [
            "id"
          ]
        }
      ]
    },
    "Machining": {
      "type": [
        "object",
        "null"
      ],
      "description": "Machining operations on a part — the same shape in the request and the result.",
      "properties": {
        "holes": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "x": {
                "type": "number",
                "description": "X position of the hole centre, from the left edge"
              },
              "y": {
                "type": "number",
                "description": "Y position of the hole centre, from the bottom edge"
              },
              "diameter": {
                "type": "number",
                "description": "Hole diameter"
              },
              "depth": {
                "type": "number",
                "description": "Hole depth. Absent for a through hole"
              },
              "face": {
                "type": "number",
                "enum": [
                  0,
                  1
                ],
                "description": "Face index (0=front, 1=back)"
              },
              "type": {
                "type": "string",
                "enum": [
                  "regular",
                  "hinge",
                  "shelf"
                ],
                "description": "Hole type"
              },
              "edge": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "l1",
                  "l2",
                  "w1",
                  "w2"
                ],
                "description": "Edge the hole is bored into. null for a face hole"
              }
            },
            "required": [
              "x",
              "y",
              "diameter"
            ]
          },
          "description": "Drilled holes"
        },
        "hingeHoles": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "position": {
                "type": "number",
                "description": "Position along the side"
              },
              "side": {
                "type": "string",
                "enum": [
                  "l1",
                  "l2",
                  "w1",
                  "w2"
                ],
                "description": "Side the hinge is placed on"
              },
              "face": {
                "type": "number",
                "enum": [
                  0,
                  1
                ],
                "description": "Face index (0=front, 1=back)"
              },
              "numHoles": {
                "type": "number",
                "description": "Number of holes in the hinge"
              },
              "diameter": {
                "type": "number",
                "description": "Hole diameter"
              },
              "depth": {
                "type": "number",
                "description": "Hole depth"
              },
              "hingeLength": {
                "type": "number",
                "description": "Length of the hinge"
              },
              "distanceFromEdge": {
                "type": "number",
                "description": "Distance from the edge to the hole centre"
              },
              "outerSpacing": {
                "type": "number",
                "description": "Spacing from the hinge ends to the outer holes"
              }
            },
            "required": [
              "position",
              "side"
            ]
          },
          "description": "Hinge hole sets"
        },
        "corners": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "index": {
                "type": "integer",
                "enum": [
                  0,
                  1,
                  2,
                  3
                ],
                "description": "Corner (0=bottom-left, 1=bottom-right, 2=top-right, 3=top-left)"
              },
              "type": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "radius",
                  "bevel"
                ],
                "description": "Corner modification"
              },
              "size": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Size of the corner modification"
              }
            }
          },
          "description": "Corner modifications"
        },
        "grooves": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "direction": {
                "type": "string",
                "enum": [
                  "l",
                  "w"
                ],
                "description": "Groove direction (l=along the length, w=along the width)"
              },
              "position": {
                "type": "number",
                "description": "Distance from the near edge, perpendicular to the groove"
              },
              "start": {
                "type": "number",
                "description": "Start offset along the groove"
              },
              "length": {
                "type": "number",
                "description": "Groove length. Absent for the full side"
              },
              "width": {
                "type": "number",
                "description": "Groove width"
              },
              "depth": {
                "type": "number",
                "description": "Groove depth"
              },
              "face": {
                "type": "number",
                "enum": [
                  0,
                  1
                ],
                "description": "Face index (0=front, 1=back)"
              }
            }
          },
          "description": "Grooves"
        }
      }
    },
    "Material": {
      "type": "object",
      "description": "A material with a saw association — the unit a MIXED job is partitioned and optimised by. Associate it to a saw with `sawId` OR an inline `saw` (never both); with neither, it runs on the job's default saw.",
      "properties": {
        "id": {
          "type": "string",
          "description": "Material identifier stock/parts reference via their `materialId`. Defaults to `name`."
        },
        "name": {
          "type": "string",
          "description": "Material name. Stock/parts without a `materialId` attach by matching their `material` (and thickness, when `thicknesses` is set)."
        },
        "type": {
          "type": "string",
          "enum": [
            "sheet",
            "linear",
            "roll"
          ],
          "default": "sheet",
          "description": "Stock type of this material — must equal the associated saw's `stockType`."
        },
        "sawId": {
          "type": "string",
          "description": "Reference to an identified saw in the `saw` array. Mutually exclusive with inline `saw`."
        },
        "saw": {
          "$ref": "#/$defs/Saw",
          "description": "Inline saw for this material (hoisted to an identified saw with id = material id). Mutually exclusive with `sawId`."
        },
        "thicknesses": {
          "type": "array",
          "items": {
            "type": "number",
            "minimum": 0
          },
          "description": "Thicknesses this material is available in — used for name-based association."
        },
        "grain": {
          "type": "string",
          "enum": [
            "l",
            "w",
            ""
          ],
          "description": "Grain direction of the material."
        }
      },
      "required": [
        "name"
      ]
    },
    "Part": {
      "type": "object",
      "description": "Part to cut",
      "properties": {
        "name": {
          "type": "string",
          "description": "Part name"
        },
        "l": {
          "type": "number",
          "description": "Length (long side dimension)",
          "minimum": 0
        },
        "w": {
          "type": "number",
          "description": "Width (short side dimension)",
          "minimum": 0
        },
        "t": {
          "description": "Thickness (z-axis dimension)",
          "type": [
            "number",
            "null"
          ],
          "minimum": 0
        },
        "material": {
          "description": "Material name",
          "type": "string",
          "maxLength": 200
        },
        "materialId": {
          "type": "string",
          "description": "Id of the request `materials` entry this part belongs to (mixed jobs). Without it, association falls back to matching `material` name + thickness."
        },
        "q": {
          "type": "number",
          "description": "Quantity",
          "default": 1,
          "minimum": 1
        },
        "trim": {
          "$ref": "#/$defs/Trim",
          "description": "Trim allowances using l1/l2/w1/w2 coordinates"
        },
        "notes": {
          "type": "string",
          "maxLength": 200,
          "description": "Part notes"
        },
        "outline": {
          "type": [
            "array",
            "null"
          ],
          "description": "Polygon outline for IRREGULAR NESTING (`saw.cutType: \"nesting\"`). Ordered vertices in part-local coordinates; the ring is closed implicitly, so do not repeat the first point. Minimum 3 vertices. When present the part is nested as its true shape; when absent the part is nested as the `l`×`w` rectangle. Ignored by every other cutType.",
          "items": {
            "type": "object",
            "properties": {
              "x": {
                "type": "number",
                "description": "Vertex X in part-local coordinates"
              },
              "y": {
                "type": "number",
                "description": "Vertex Y in part-local coordinates"
              }
            },
            "required": [
              "x",
              "y"
            ]
          }
        },
        "holes": {
          "type": [
            "array",
            "null"
          ],
          "description": "Interior voids of an irregular-nesting part — an array of polygon rings, same vertex convention as `outline`. Each ring is cut but never packed into. Only meaningful alongside `outline`; distinct from `machining.holes`, which are drilled operations rather than geometry.",
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number",
                  "description": "Vertex X in part-local coordinates"
                },
                "y": {
                  "type": "number",
                  "description": "Vertex Y in part-local coordinates"
                }
              },
              "required": [
                "x",
                "y"
              ]
            }
          }
        },
        "code": {
          "type": "string",
          "description": "Internal product or material code. Mapped to PRODUCT in PTX export."
        },
        "labels": {
          "type": "object",
          "description": "Typed per-part saw label-printer fields (with top-level `labels.customer` for the job). Emitted by PTX (named PARTS_INF columns) and Mayer .MAY (MA010 text slots 1-9); Biesse XML to follow. Schema may change; see `/v3/export/ptx` and `/v3/export/mayer`.",
          "properties": {
            "barcode1": {
              "type": "string",
              "maxLength": 50,
              "description": "CNC program side 1 (PARTS_INF.BARCODE1 in PTX)"
            },
            "barcode2": {
              "type": "string",
              "maxLength": 50,
              "description": "CNC program side 2 (PARTS_INF.BARCODE2 in PTX)"
            },
            "room": {
              "type": "string",
              "maxLength": 50,
              "description": "Room or location label (PARTS_INF.ROOM in PTX)"
            },
            "pallet": {
              "type": "string",
              "maxLength": 50,
              "description": "Pallet identifier (PARTS_INF.PALLET in PTX)"
            },
            "drawing": {
              "type": "string",
              "maxLength": 50,
              "description": "Drawing reference (PARTS_INF.DRAWING in PTX)"
            },
            "order": {
              "type": "string",
              "maxLength": 50,
              "description": "Order number (PARTS_INF.ORDER in PTX)"
            },
            "colour": {
              "type": "string",
              "maxLength": 50,
              "description": "Colour label (PARTS_INF.COLOUR in PTX)"
            },
            "productInfo": {
              "type": "string",
              "maxLength": 50,
              "description": "Product info (PARTS_INF.PROD_INFO in PTX)"
            },
            "productNumber": {
              "type": "string",
              "maxLength": 50,
              "description": "Production number (PARTS_INF.PROD_NUM in PTX)"
            }
          }
        },
        "customData": {
          "type": "object",
          "additionalProperties": true,
          "description": "Custom user-defined data. All entries are written to PARTS_UDI (INFO1..INFO60) in PTX export. Note: `barcode`/`barcode1`/`barcode2`/`clientName` here are legacy fallbacks — prefer the typed `labels` field above."
        },
        "extras": {
          "$ref": "#/$defs/Extras",
          "description": "Extras structure (required for banding, finish, etc.)"
        },
        "machining": {
          "$ref": "#/$defs/Machining"
        },
        "orientationLock": {
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "",
            "l",
            "w"
          ],
          "description": "Orientation lock for rotation constraints"
        },
        "upDirection": {
          "description": "Up direction indicator",
          "type": [
            "string",
            "null"
          ],
          "enum": [
            "l",
            "w"
          ]
        },
        "stockLock": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "default": [],
          "description": "Parent IDs of stock(s) this part is locked to"
        }
      },
      "required": [
        "l",
        "w"
      ]
    },
    "Saw": {
      "type": "object",
      "description": "Saw configuration. Settings that do not apply to each other are RESOLVED rather than rejected — the saw-rules registry clamps, defaults or zeroes them and the response `warnings` name the rule that fired. Browse every rule, and try your own saw against them, at https://smartcut.dev/api-docs/saw-settings",
      "properties": {
        "vendor": {
          "type": "string",
          "description": "Saw vendor (e.g. Biesse, Mayer, SCM). Part of the identity pair (`vendor` + `model`) used to look up this saw's profile, which supplies its algorithm constraints and preferences. Optional — without an identity (or a `sawProfileId`) no profile is applied."
        },
        "model": {
          "type": "string",
          "description": "Saw model name (e.g. PC7000, OptiPlanner, Pratika). Paired with `vendor` to resolve the saw profile — `GET /v3/saws` lists the recognised pairs."
        },
        "softwareVersion": {
          "type": "string",
          "description": "Saw control software version (e.g. \"CADmatic 5\") — selects the CADmatic output dialect where recognised (5 → CAD5/PTNR, 3|4 → CAD4/PTN3), otherwise provenance only."
        },
        "cutType": {
          "type": "string",
          "enum": [
            "efficiency",
            "guillotine",
            "beam",
            "nesting"
          ],
          "description": "Type of cutting algorithm. `guillotine` and `beam` produce edge-to-edge saw cuts; `efficiency` packs without the guillotine constraint; `nesting` is IRREGULAR nesting for CNC routers and similar — give each part an `outline` (and optional `holes`) and it is packed as its true shape rather than its bounding rectangle."
        },
        "cutPreference": {
          "type": "string",
          "enum": [
            "l",
            "w",
            "flex"
          ],
          "description": "Preferred cutting direction. Only applicable when cutType is 'guillotine' or 'beam'. Set to 'flex' to try both 'l' and 'w' on a per-sheet basis and keep the better orientation per sheet — charged at 2× the standard credit rate."
        },
        "bladeWidth": {
          "type": "number",
          "minimum": 0,
          "description": "Width of the saw blade"
        },
        "stockType": {
          "type": "string",
          "enum": [
            "sheet",
            "linear",
            "roll"
          ],
          "description": "Type of stock material"
        },
        "minOffcut": {
          "type": [
            "object",
            "null"
          ],
          "description": "Per-job override of this saw's minimum reusable offcut size, in the same units as every other dimension you send. A saw profile (`sawProfileId`) may already declare its own floor — sending this REPLACES it entirely for the job; the profile's value is only a default. Every leftover still ships in `offcuts`; a floor only decides each one's `retained` flag (whether this saw's operator would re-rack it), never geometry or board count. Compared orientation-free: `{ l: 150, w: 100 }` reads \"no side under 100, longer side at least 150\" — 160×95 and 95×160 give the same verdict. `area` is an independent third check. Absent → the resolved saw profile's own floor, or none.",
          "properties": {
            "l": {
              "type": "number",
              "minimum": 0,
              "description": "Minimum long side of a reusable offcut"
            },
            "w": {
              "type": "number",
              "minimum": 0,
              "description": "Minimum short side of a reusable offcut"
            },
            "area": {
              "type": "number",
              "minimum": 0,
              "description": "Minimum area of a reusable offcut, independent of side length"
            }
          }
        },
        "guillotineOptions": {
          "type": "object",
          "description": "Guillotine-specific options. Only applicable when cutType is 'guillotine' or 'beam'",
          "properties": {
            "strategy": {
              "type": "string",
              "enum": [
                "efficiency",
                "time"
              ],
              "default": "efficiency",
              "description": "Cutting strategy"
            },
            "maxPhase": {
              "type": "number",
              "minimum": 0,
              "maximum": 10,
              "default": 0,
              "description": "Maximum cutting phase — the number of turns a board may take: 0 = unlimited, 1 = two-stage cutting (rips, one turn, crosscuts — every strip takes only parts whose cross dimension matches the strip), 2 = three-stage, … Enforced exactly as sent for every cut type, except where a saw profile (sawProfileId) states the saw's own limit: then 0 takes the profile's default, and on a saw whose control counts its own turns a higher value is lowered to the saw's ceiling. Mayer (mayer-may, PC7000 control): default 3, maximum 4 — 4 is opt-in, because the PC7000 has no automatic turn table and each extra turn is one more piece the operator turns and feeds by hand; it saves stock on some jobs (7 of 713 in our Mayer test set, one sheet each). A Max `model` runs only when the resulting cap is 0 (the Mayer and Biesse profiles default to 3, so 0 there still means 3) — any cap runs the Fast model; on a beam saw it also needs a recognised saw profile."
            },
            "adaptiveMaxPhase": {
              "type": "boolean",
              "description": "Let the optimiser raise maxPhase one turn at a time, up to the saw control's own turn ceiling (Mayer PC7000: 4), when a deeper cap gives FEWER sheets, or the SAME sheets in less predicted cutting time (equal time: the larger offcut wins). It never returns more sheets than the maxPhase you set, and on a saw with no cutting-time model it raises the cap only to save a sheet. Applies only to a saw profile whose control counts its own turns and has a ceiling deeper than the job's cap (today: mayer-may); elsewhere it does nothing. The caps tried, and which one was used and why, are reported in metadata.phaseStep. Omit it to use your account default (off)."
            },
            "timeObjective": {
              "type": "boolean",
              "description": "Plan for less cutting time as well as material. The job is also planned material-first, and the faster plan is used only when it needs no more sheets and, at equal sheets, is predicted to cut faster. This roughly doubles the compute for the job, and compute is billed. Applies only to a saw profile with a cutting-time model (today: mayer-may); elsewhere it does nothing. The outcome is reported in optimisation.productionTime.guard. Omit it to use your account default (off)."
            },
            "duplicateStrips": {
              "type": "boolean",
              "description": "Convenience option: where the job holds another full copy of a strip's parts, place the identical strip next to it on the same sheet, so a saw that stacks identical strips (for example a Mayer PC7000 control) can cut the two together. It groups identical strips for stacking and may use more material than the default plan. Applies to guillotine and beam saws. Omit it to use your account default (off)."
            },
            "headCuts": {
              "type": "boolean",
              "default": false,
              "description": "Enable head cuts"
            },
            "limitStripDimensions": {
              "type": "boolean",
              "default": false,
              "description": "Prefer narrower strips, derived from the job's own parts (mean part depth, floored at a fifth of the board). Carries no dimension of its own — use maxStripWidth to state an operator limit. With both set the TIGHTER of the two wins: on a job of small parts the derived cap is usually well below any operator limit, and maxStripWidth then has no effect at all. To make your own number the cap, send it with this flag off."
            },
            "verticalPanelSaw": {
              "type": "boolean",
              "default": false,
              "description": "Arrange the layout for a vertical panel saw: narrowest strips at the top, residual waste at the bottom, cut from the top down. Works on any saw; no profile required."
            },
            "maxStripWidth": {
              "type": [
                "number",
                "null"
              ],
              "minimum": 0,
              "default": null,
              "description": "Widest assembled strip an operator can safely handle, in the same units as every other dimension you send. The optimiser steers away from strips wider than this — it opens strips on parts within the limit wherever it can. Set this alone (limitStripDimensions off) and your number IS the cap. With limitStripDimensions also on, the tighter of the two wins — the parts-derived cap is often tighter, and then this value has no effect. It is a STRONG PREFERENCE, not a guarantee — a part whose own smaller side exceeds the limit cannot be made to fit one, and layouts are never left unplaced to satisfy it. Hard rejection of an over-wide strip requires a saw profile (sawProfileId), whose oracle reports strip-width-exceeded. Null = no limit."
            }
          }
        },
        "efficiencyOptions": {
          "type": "object",
          "description": "Efficiency-specific options. Only applicable when cutType is 'efficiency'",
          "properties": {
            "primaryCompression": {
              "type": "string",
              "enum": [
                "l",
                "w"
              ],
              "default": "w",
              "description": "Primary compression direction (l=length/horizontal, w=width/vertical)"
            }
          }
        },
        "stackHeight": {
          "type": "number",
          "default": 100,
          "description": "The saw's maximum cutting height, in the same units as every other dimension you send. Sets how many sheets one pattern may be cut through at once: `⌊stackHeight ÷ sheet thickness⌋`. Applies to any saw, not only beam saws. Only read when `options.stackingMode` is not `none`."
        },
        "options": {
          "type": "object",
          "description": "General optimisation options: which stock is chosen, whether sheets with the same pattern are cut as a stack, and spacing between parts.",
          "properties": {
            "stockSelection": {
              "type": "string",
              "enum": [
                "efficiency",
                "smallest"
              ],
              "default": "efficiency",
              "description": "How the optimiser chooses between the stock sizes you send.\n\n- `efficiency`: pick the stock that gives the best material utilisation.\n- `smallest`: use the smallest stock (by area) that the parts fit on first."
            },
            "offcutPriority": {
              "type": "number",
              "minimum": 0,
              "maximum": 1,
              "default": 0.5,
              "description": "Offcut priority — how stock rows marked `offcut: true` are used against full sheets. 0 = never use offcut stock (offcut rows are left out of the job), 0.5 = offcuts compete with full sheets on score (default), 1 = always use offcuts first (an offcut is taken whenever anything fits it). Between 0.5 and 1 an offcut is taken when its fill is at least (1 − k) of the best full sheet's fill, k = (p − 0.5) × 2; values between 0 and 0.5 currently behave as 0.5. Has no effect unless some stock rows are marked `offcut: true`, and is ignored for a material whose stock rows are ALL offcuts (there is no full sheet to choose instead)."
            },
            "stackingMode": {
              "type": "string",
              "enum": [
                "dimensions",
                "identical",
                "none"
              ],
              "default": "dimensions",
              "description": "Whether sheets that share a cutting pattern are cut together as a **stack**: several sheets laid on top of each other and cut once. In the result, a stack is one `stock` entry whose `stack` is the number of sheets, and each part cut from it carries `stacked: { sheet, of }`.\n\n- `identical`: stack only when every position holds the same part (same size and material) on every sheet.\n- `dimensions`: stack when every position holds a part of the same size. The parts may be different parts, so each sheet of the stack can yield different parts.\n- `none`: never stack. Every sheet is cut individually.\n\nStack depth is capped at `⌊saw.stackHeight ÷ thickness⌋`, and at `options.maxStackSheets` when set. Thickness is the stock's `t`, or else the thickest part on the sheet. When neither is known, the height does not limit the stack. Stacking is also off when `guillotineOptions.strategy` is `time`.\n\n**Grain across a stack.** Every sheet of a stack is cut to one pattern, so each position has one place and one orientation on every sheet. A part fills a position on another sheet only if it may take that orientation: a part locked to the grain (`orientationLock`) joins a stack only where the pattern's orientation keeps its grain on that stock, so grain is never broken to make a stack. Under `identical` the part on every sheet must also carry the same `orientationLock`.\n\nThe result lists every stack in `stacks`: its depth, and each sheet with its number in the job."
            },
            "maxStackSheets": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 1,
              "description": "The most sheets one stack may hold, whatever their thickness. Applied on top of the height cap `⌊stackHeight ÷ thickness⌋` — the smaller of the two wins. Absent or null = no count limit (the height cap alone)."
            },
            "minSpacing": {
              "type": "number",
              "minimum": 0,
              "default": 0,
              "description": "Minimum gap between parts (not between a part and the stock edge), in the same units as every other dimension you send. The blade width is the floor: the gap used is the larger of `bladeWidth` and this value, not their sum. Only applies when `cutType` is `efficiency`. Other cut types space parts by the blade width, so the value is set to 0 and a warning is returned."
            },
            "minSpacingDimension": {
              "type": "object",
              "description": "A different minimum spacing for small parts. Parts whose length or width is at or below `dimension` use this `minSpacing` instead of the general one, still floored at the blade width. Only applies when `cutType` is `efficiency`.",
              "properties": {
                "dimension": {
                  "type": "number",
                  "minimum": 0,
                  "description": "Parts with length or width equal to or smaller than this value will use the spacing below"
                },
                "minSpacing": {
                  "type": "number",
                  "minimum": 0,
                  "description": "Minimum spacing for parts meeting the dimension threshold"
                }
              }
            }
          }
        }
      },
      "required": [
        "bladeWidth",
        "stockType"
      ]
    },
    "SideValues": {
      "type": "object",
      "description": "Side values",
      "properties": {
        "l1": {
          "oneOf": [
            {
              "type": "boolean"
            },
            {
              "type": "string"
            }
          ],
          "description": "Long side 1"
        },
        "l2": {
          "oneOf": [
            {
              "type": "boolean"
            },
            {
              "type": "string"
            }
          ],
          "description": "Long side 2"
        },
        "w1": {
          "oneOf": [
            {
              "type": "boolean"
            },
            {
              "type": "string"
            }
          ],
          "description": "Short side 1"
        },
        "w2": {
          "oneOf": [
            {
              "type": "boolean"
            },
            {
              "type": "string"
            }
          ],
          "description": "Short side 2"
        }
      }
    },
    "Stock": {
      "type": "object",
      "description": "Stock item",
      "properties": {
        "name": {
          "type": "string",
          "description": "Stock name"
        },
        "l": {
          "type": "number",
          "description": "Length (long side dimension)",
          "minimum": 0
        },
        "w": {
          "type": "number",
          "description": "Width (short side dimension)",
          "minimum": 0
        },
        "t": {
          "description": "Thickness (z-axis dimension)",
          "type": [
            "number",
            "null"
          ],
          "minimum": 0
        },
        "material": {
          "description": "Material name",
          "type": "string",
          "maxLength": 200
        },
        "materialId": {
          "type": "string",
          "description": "Id of the request `materials` entry this stock belongs to (mixed jobs). Without it, association falls back to matching `material` name + thickness."
        },
        "q": {
          "type": "number",
          "description": "Quantity",
          "default": 1,
          "minimum": 1
        },
        "grain": {
          "description": "Grain direction of the material",
          "default": "",
          "type": "string",
          "enum": [
            "l",
            "w",
            ""
          ]
        },
        "trim": {
          "$ref": "#/$defs/Trim",
          "description": "Trim allowances using l1/l2/w1/w2 coordinates"
        },
        "cost": {
          "type": [
            "number",
            "null"
          ],
          "description": "Cost per unit. Carried through to the result for pricing and reports; it does not currently influence which stock the optimiser chooses (equal choices fall to the order the stock is listed in).",
          "default": 0,
          "minimum": 0
        },
        "notes": {
          "description": "Description",
          "type": "string",
          "maxLength": 200
        },
        "code": {
          "type": "string",
          "description": "Material or product code from supplier. Mapped to BOARDS CODE in PTX export."
        },
        "customData": {
          "type": "object",
          "additionalProperties": true,
          "description": "Custom user-defined data."
        },
        "autoAdd": {
          "type": "boolean",
          "description": "Automatically add stock as needed",
          "default": false
        },
        "infiniteLength": {
          "type": "boolean",
          "description": "Whether stock has infinite length (e.g., for rolls)",
          "default": false
        },
        "allowExactFitShapes": {
          "description": "Allow parts that exactly fit the stock dimensions even when trim is applied",
          "type": "boolean"
        },
        "offcut": {
          "type": "boolean",
          "default": false,
          "description": "Marks this stock row as an offcut (a remnant from earlier jobs) rather than a full sheet. `saw.options.offcutPriority` decides how offcut rows are used against full sheets: 0 leaves them out of the job, 0.5 (default) lets them compete on score, 1 takes an offcut whenever anything fits it. Without any row marked `offcut: true`, `offcutPriority` has no effect; when a material's rows are all offcuts, they are all used whatever the priority."
        }
      },
      "required": [
        "l",
        "w"
      ]
    },
    "Trim": {
      "type": "object",
      "description": "Trim allowances",
      "properties": {
        "l1": {
          "type": "number",
          "minimum": 0,
          "default": 0,
          "description": "Long side 1 trim value"
        },
        "l2": {
          "type": "number",
          "minimum": 0,
          "default": 0,
          "description": "Long side 2 trim value"
        },
        "w1": {
          "type": "number",
          "minimum": 0,
          "default": 0,
          "description": "Short side 1 trim value"
        },
        "w2": {
          "type": "number",
          "minimum": 0,
          "default": 0,
          "description": "Short side 2 trim value"
        }
      }
    }
  }
}