# 3D model analysis

URL: https://smartcut.dev/docs/model-analysis

> Upload a STEP, STL, OBJ, PLY or glTF model to the SmartCut API and get back a parts list you can submit straight to /v3/calculate, with advice on shaped parts, fittings and overlapping solids.

Upload a 3D model. Get back a parts list.

`POST /v3/model/analysis` reads a CAD file, works out which solids in it are panels, measures them, and returns them in exactly the shape [`/v3/calculate`](/api-docs/v3) wants. The `parts` array it gives you **is** that endpoint's `parts` array. No field mapping, no conversion.

```text
POST https://api.smartcut.dev/v3/model/analysis
```

## The request is the file

The body is the model file itself, as raw bytes. Not a multipart form, not base64 inside JSON. There is exactly one file and nothing else to send, so everything else is a query parameter.

```bash
curl -X POST \
  "https://api.smartcut.dev/v3/model/analysis?filename=kitchen.step&units=mm" \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @kitchen.step
```

| Parameter | Default | What it does |
|---|---|---|
| `filename` | none | The file's name. Used to work out the format when the bytes carry no magic. |
| `format` | none | Force the format instead of inferring it. Magic bytes still win where the file has them. |
| `units` | `mm` | The unit every number in the response is in. One of `mm`, `cm`, `m`, `in`, `ft`. |
| `sourceUnits` | none | The unit the model was **drawn** in. Overrides whatever the file declares. |
| `dedupe` | `true` | Collapse identical parts into one row with a quantity, which is what a cut list wants. |
| `mirrorDedupe` | `false` | Treat a part and its mirror image as the same part. |
| `bladeWidth` | none | Your saw's kerf, in `units`. Enables the units sanity check below. |

## Formats

The format is decided by the file's **magic bytes**, with the extension as a tiebreak. People rename files, and a `.stl` that is really a zip should fail with something better than a parser crash. OBJ and ASCII STL carry no magic at all, so for those you must send `filename` or `format`. Without one the request is refused rather than guessed at.

| Format | Extensions | Supported | Notes |
|---|---|---|---|
| STEP | `.step`, `.stp` | Yes | **The best input.** Exact B-rep solids, tessellated server-side, and the file declares its own unit, so `sourceUnits` is not needed and cannot be got wrong. |
| STL | `.stl` | Yes | Binary and ASCII. Carries no part names and no materials, so parts are separated by geometry alone. |
| OBJ | `.obj` | Yes | Keeps object names and `usemtl` material names. |
| PLY | `.ply` | Yes | Geometry only. |
| glTF / GLB | `.gltf`, `.glb` | Yes | Embedded-buffer GLB works. External `.bin` or texture files are not fetched, so geometry only. |
| Collada | `.dae` | **No, `415`** | Needs a browser DOM to parse. Convert to STEP or STL. |
| 3MF | `.3mf` | **No, `415`** | Same reason. Convert to STEP or STL. |

A `415` is not a dead end. It names what would have worked:

```json
{
  "error": "The DAE format cannot be read here. Supported formats: obj, stl, ply, gltf, step.",
  "details": {
    "format": "dae",
    "supported": [ "obj", "stl", "ply", "gltf", "step" ],
    "help": "Re-export the model as STEP (best — exact solids and real units) or STL. Collada and 3MF can only be read in the browser."
  },
  "version": "3"
}
```

## Units

Only STEP declares a unit the server can act on. Every mesh format (STL, OBJ, PLY, glTF) carries **nothing**, so if you do not say what the model was drawn in, the numbers are taken to be already in `units`.

Getting this wrong is silent. Every part stays consistent with every other, so a cabinet exported from a tool set to metres and read as millimetres imports as a set of perfectly self-consistent, thousand-times-too-small parts. It is worth being explicit:

```text
?filename=cabinet.obj&sourceUnits=m&units=mm
```

The response always tells you what it did:

```json
"model": {
  "format": "obj",
  "units": "mm",
  "sourceUnits": "m",
  "declaredUnits": null
}
```

Send `bladeWidth` and the server can also catch the mistake for you. A kerf is a fraction of a percent of anything worth cutting, so a blade that is an appreciable slice of the whole model means the model is not small. The unit is wrong. When that happens, `model.suggestedUnits` names the `sourceUnits` that would make sense of it. Without a `bladeWidth` there is nothing to measure against and no suggestion is made.

## The response

```json
{
  "version": "3",
  "model": {
    "format": "step",
    "units": "mm",
    "sourceUnits": "mm",
    "declaredUnits": "mm",
    "stats": {
      "parts": 3, "rectangular": 2, "irregular": 1, "unrecognised": 0,
      "fixtures": 0, "merged": 2, "triangles": 4820,
      "components": 5, "debris": 0,
      "modelDiagonal": 1523.4,
      "modelSize": { "x": 1200, "y": 600, "z": 720 }
    }
  },
  "parts": [
    { "name": "SIDE", "l": 720, "w": 560, "t": 18, "q": 2, "material": "MDF", "role": "rect" },
    { "name": "SHELF", "l": 564, "w": 540, "t": 18, "q": 1, "material": "MDF", "role": "rect" },
    { "name": "CURVED TOP", "l": 600, "w": 300, "t": 18, "q": 1, "material": "OAK", "role": "nesting",
      "outline": [ { "x": 0, "y": 0 }, { "x": 600, "y": 0 } ] }
  ],
  "advice": [
    { "code": "squared-off-waste", "severity": "info", "partIndex": 2, "wastePct": 0.21,
      "message": "Squaring \"CURVED TOP\" off to a rectangle wastes 21.0% of its area. Send its outline to a nesting model to keep it." }
  ],
  "warnings": []
}
```

### Parts

Every field on a part except `role` is a `/v3/calculate` part field. `role` is not, and `/v3/calculate` **ignores fields it does not recognise** rather than rejecting them, so you can post the array back exactly as it arrived. You do not have to strip anything:

```bash
# 1. analyse
curl -sS -X POST \
  "https://api.smartcut.dev/v3/model/analysis?filename=kitchen.step" \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @kitchen.step > analysis.json

# 2. cut it. The parts array goes straight across
jq '{
  saw:   { cutType: "guillotine", cutPreference: "l", bladeWidth: 3.2, stockType: "sheet" },
  stock: [ { l: 2440, w: 1220, t: 18, material: "MDF", q: 20 } ],
  parts: .parts
}' analysis.json > calculate.json

curl -X POST https://api.smartcut.dev/v3/calculate \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  --data @calculate.json
```

`role` says what each part is **for**, and it is the one thing worth acting on before you cut:

| `role` | Meaning | What to do |
|---|---|---|
| `rect` | A plain rectangle. | Nothing. Cut it. |
| `nesting` | A shaped part. `outline` (and `holes`) carry its real geometry. | Submit it to a nesting model to keep the shape, or let a guillotine model square it off to `l` x `w` and accept the waste. `advice` tells you how much that costs. |
| `fixture` | A fitting such as a handle, a hinge or a knob, never cut from sheet at all. | **Filter these out** before cutting. A `fixture-detected` entry in `advice` names each one. |

```bash
# drop the fittings
jq '[ .parts[] | select( .role != "fixture" ) ]' analysis.json
```

### Advice

`advice` is everything the analysis knows that a calculation does not, as data rather than prose. Each entry has a stable `code`, a `severity` of `info`, `warning` or `error`, a human-readable `message`, and, where it is about a specific part, a `partIndex` into `parts`.

| Field | When |
|---|---|
| `partIndex` | The advice is about one part. Absent when it is about the file as a whole. |
| `otherPartIndex` | The advice is about a **pair**, currently only `parts-overlap`. |
| `penetrationFraction` | On `parts-overlap`: how deeply, as a fraction of the smaller part's smallest dimension. |
| `wastePct` | On `squared-off-waste`: the fraction of the bounding rectangle that is waste, 0..1. |
| `confidence` | On `fixture-detected` / `role-uncertain`: how sure the verdict was, 0..1. |

Codes worth handling:

| Code | Severity | Means |
|---|---|---|
| `fixture-detected` | `info` | This is a fitting, not sheet goods. Drop it. |
| `role-uncertain` | `info` | The fitting question was close. Worth a human look. |
| `squared-off-waste` | `info` | Squaring this shaped part off costs `wastePct` of its area. |
| `dimensions-approximate` | `warning` | The dimensions came from a fallback, not from measured flat faces. Check them. |
| `parts-overlap` | `warning` | Two parts share space. Often deliberate (a housing, a rebate, a biscuit), so this is never a blocker. |
| `not-flat-panel` | `warning` | Turned, curved, tapered or multi-body. The bounding box is the best available answer. |
| `open-mesh` / `non-manifold-edge` | `warning` | The mesh is not a closed solid. Dimensions may be unreliable. |
| `model-too-heavy` | `warning` | Very dense. It worked, but a coarser export would be faster. |
| `scanned-mesh-file` | `error` | This is a scan or a conversion, not a CAD model of parts. |

## Limits and cost

| | |
|---|---|
| **Maximum upload** | 32MB. Larger bodies get `413` without being buffered. |
| **Maximum complexity** | 2,000,000 triangles. Above that, `422`. Export with a coarser mesh setting, or decimate. |
| **Time budget** | 30 seconds. A model that does not finish returns `422` with the same advice. |
| **Rate limit** | The same per-minute budget as `/v3/calculate`. |
| **Billing** | The same compute-time meter as a calculation, and it counts against your monthly call allowance. |

A refused request is **not billed** and does not spend a call: an unsupported format, an over-size model, a timeout.

## Errors

Every error is `{ "error": "…", "details": { … }, "version": "3" }`.

| Status | Means |
|---|---|
| `400` | The body is not a file, the format could not be determined, or `units`/`sourceUnits` are not ones we convert. |
| `401` | Missing or unknown API key. |
| `403` | Over the monthly call limit, or the account is on hold. |
| `413` | Body larger than 32MB. |
| `415` | A format this server cannot read. `details.supported` lists the ones that work. |
| `422` | Too many triangles, or the analysis did not finish in its budget. |
| `429` | Rate limited. Wait and retry. |

## Related

- [API reference](/api-docs/v3): the generated schema for this endpoint and every other.
- [Guide](/docs/guide/): getting started with the optimisation API.
- [Result webhooks](/docs/webhooks/): how to receive the calculation you submit next.
