---
title: "Image or text → real 3D-printable CAD model (STL)"
tier: hero
format: cad-3d-print
theme: hardware | product-design | maker | fabrication
persona: maker, product-designer, hardware-engineer, fab-shop, educator
duration: a prompt or a reference image → a watertight, slicer-ready STL + 4-view render + printability report in 60–180s
budget_usd: $0.05–$0.50 per part (priced per render + per refine pass)
caps: ["create_cad_model", "refine_cad_model", "export_stl", "cad-render", "cad-validate", "flux-dev", "gpt-image"]
skills: ["livepeer-cli"]
showcases:
  - title: "Text → STL · a parametric phone stand"
    href: "/chapters/text-image-to-3d-print-example.html"
  - title: "Image → STL · a low-poly desk hook from a generated reference"
    href: "/chapters/text-image-to-3d-print-example.html"
status: live — both showcase STLs pass the watertight + manifold gate (trimesh is_volume). See public/chapters/text-image-to-3d-print/.
reliability: 4.9 # 5 − .3 brief→OpenSCAD compile (generative, but gated by cad-validate) − .3 optional reference image gen; render/validate/export deterministic tools; +.5 proven E2E showcase (both STLs pass is_volume)
---

# Image or text → a real, printable STL

Most "AI 3D" produces a *mesh you can look at* — a soupy, non-manifold blob from photogrammetry or a learned implicit field. It renders fine in a browser and **fails the moment a slicer touches it**: holes, flipped normals, zero wall thickness, no flat base. You can't print it without an hour of mesh-repair in Blender or Meshmixer.

`cad-render` takes the other road. It turns your intent (a text prompt **or** a reference image) into **parametric CAD code** (OpenSCAD), then renders that code to a genuinely **watertight, manifold, millimetre-accurate STL** — the kind a slicer ingests with zero repair. Because the geometry is *code*, it's also editable: change "wall = 2mm" and re-render, instead of re-sculpting.

> **`cad-render`** is the OpenSCAD-on-the-Livepeer-network capability: it accepts OpenSCAD source (or a brief the agent compiles into OpenSCAD), runs it headless, and returns `{ stl_url, ... }`. **`cad-validate`** is the gate — it loads the STL and reports `n_vertices`, `n_faces`, `is_watertight`, `is_winding_consistent`, `is_volume` (true ⇒ a real solid), `euler_number`, `volume_mm3`, `area_mm2`, and `bbox_mm`. The agent renders the 4-view orthographic preview from the returned STL for review (`is_volume: true` is the only thing that gates shipping).

## Showcases

- **[Text → STL · parametric phone stand](/chapters/text-image-to-3d-print-example.html)** — a clean functional part from one sentence. Watertight, manifold, flat base, ≥2mm walls. _Passes the gate._
- **[Image → STL · low-poly desk hook](/chapters/text-image-to-3d-print-example.html)** — generate a reference image with `flux-dev`, drive `cad-render` from it, self-critique the 4-view, refine, re-validate. _Passes the gate._

## When to reach for which — code-CAD vs. a learned mesh

| You want… | Use | Why |
|---|---|---|
| A **functional, printable** part — bracket, stand, hook, organizer, enclosure, jig, knob | **`cad-render` (code-CAD)** | Parametric OpenSCAD is watertight + manifold *by construction*. Dimensions are exact mm. Walls are whatever you set them to. A slicer accepts it with zero repair. |
| An **organic / sculptural / hero** asset for a render or a game — a creature, a face, a gnarled tree | **`tripo-i3d` / `triposplat`** (mesh / Gaussian-splat) | These capture organic surface detail code-CAD can't. But the output is a *display* mesh — expect to repair it before printing, if it's printable at all. |
| A part you'll **edit later** ("make the wall thicker", "add 5mm to the stem") | **`cad-render`** | The geometry is code. Change one parameter and re-render. A mesh has no parameters to change. |
| A part where **fit / tolerance matters** (it has to clip onto a real object) | **`cad-render`** | You control exact mm. A learned mesh has no reliable scale. |

**Rule of thumb:** if it needs to *function* — to hold, clip, mount, stack, or fit — reach for `cad-render`. If it just needs to *look* like something, a mesh model is fine (and you probably won't print it).

## The recipe

```
text  ─┐
       ├─→  create_cad_model  ─→  4-view render  ─→  (review) ─→  refine_cad_model  ─→  export_stl (GATE) ─→  download ─→  slice / print
image ─┘                                                ↑__________________ self-critique loop __________________↑
```

1. **Describe the part** (text) **or** generate / supply a clean reference image. For image-driven parts, a low-poly, single-object, neutral-background reference works far better than a busy photo — the model is reading silhouette + proportion, not texture.
2. **`create_cad_model`** — compile the brief into OpenSCAD and render. You get back the OpenSCAD source, a **4-view PNG** (front / right / top / iso), and a **printability report**.
3. **Review the 4-view.** This is the whole game. Is the base flat? Are the walls thick enough to print? Is anything floating, or a 0-thickness shell? Does it match what you asked for?
4. **`refine_cad_model`** — pass the prior model + a one-line critique ("the base is too thin, make it 4mm and widen the foot for stability"). Re-render. Loop 1–3× until the 4-view looks right.
5. **`export_stl` — the gate.** Validate watertight + manifold (`cad-validate`, or trimesh `is_volume`). **Only ship a part that passes.** If it fails, the report tells you why (a hole, an inverted normal, a degenerate face) — feed that back into `refine_cad_model`.
6. **Download the `.stl`**, drop it in your slicer (Cura / PrusaSlicer / Bambu), slice, print.

## How the agent should run this (interaction contract)

1. **CONFIRM (one message, ≤1 question):** restate in 1 line ("printable <part> in mm, ~$0.15–0.45, ~1–3 min + 1–3 refine passes") + real choices: text brief vs image-driven (clean reference) · target dims in mm · bed clamp (`max_dim_mm`, default 120). The one question worth asking: exact dimensions/fit if the part must clip onto a real object — a wrong mm is a wasted print.
2. **PREVIEW CHECKPOINT:** show the **4-view PNG + printability report** (~$0.10) BEFORE the user slices or prints anything — the front/right views reveal the non-flat base or 0-thickness wall the iso render hides. The user's filament + print hours are the expensive stage here, not the render.
3. **NARRATE:** 1-liner per pass ("compiling brief → OpenSCAD, render ~30–60s", "refine pass 2/3: thickening base to 4mm"); each verb returns synchronously in ~30–90s, so silence is never an issue — but always report the validate verdict explicitly.
4. **FAIL GRACEFULLY:** `is_volume:false` → feed the named defect (hole / inverted normal / degenerate face) straight into `refine_cad_model`, ≤2 repair passes, then simplify the geometry brief; image-driven compile misreads a busy photo → regenerate a clean reference (`flux-dev`, single object, white background) or fall back to a text brief; MCP verbs unavailable → drive `cad-render`/`cad-validate` directly via `POST /inference`. A simpler part that passes the gate beats a fancy one that fails it.
5. **DELIVER:** STL download link + report + one honest line ("watertight + manifold; code-CAD can't do organic/sculptural surfaces — that's `tripo-i3d` territory") + ONE next step ("want a parameter tweaked — wall, height, lean — say it and I re-render").

## Manufacture-ready rules (the part has to survive a slicer AND a printer)

These are the rules `cad-render` is built to honor and `cad-validate` checks for. Bake them into your brief.

| Rule | Why | Check |
|---|---|---|
| **Units are millimetres.** | Slicers assume mm. A part authored in "units" prints at 1/25th scale. | Report `dimensions_mm` — sanity-check against your intent. |
| **Watertight + manifold.** | A non-watertight or non-manifold mesh is ambiguous to a slicer — it can't tell inside from outside. | `is_watertight: true` **and** `is_winding_consistent: true` ⇒ `is_volume: true`. **This is the gate.** |
| **Walls ≥ 1.2mm** (≥ 2mm for load-bearing). | Thinner than ~3 nozzle-widths (0.4mm nozzle ⇒ ~1.2mm) won't print solid — it ghosts or fails. | Report `min_wall_mm`. |
| **Flat base / printable footprint.** | FDM prints from the bed up. A part with no flat face needs supports (or it falls over). | Eyeball the 4-view *front/right*; prefer a flat bottom. |
| **Fits the build volume** (≤ ~256mm any axis for a typical FDM bed). | A part bigger than the bed can't print in one piece. | Report `dimensions_mm` — keep max axis under your printer's bed. |
| **No floating / disconnected islands.** | Disconnected bodies print as separate loose pieces (or in mid-air). | One connected solid in the iso view. |

## The verbs

Three MCP verbs wrap the loop. (Pre-PR-#430, drive `cad-render` / `cad-validate` directly via `POST /inference` — see *Direct calls* below.)

### `create_cad_model` — brief or image → first STL

```javascript
mcp__storyboard__create_cad_model({
  // ONE of these two:
  prompt: "a phone stand for a 7mm-thick phone, ~70mm tall, 60° lean, flat triangular base, 3mm walls",
  // image_url: "https://.../reference.png",   // image-driven: silhouette + proportion

  units: "mm",
  max_dim_mm: 120,            // clamp into the build volume
  min_wall_mm: 2.0,           // enforce printable walls
  // returns: { openscad_source, stl_url, render_url (4-view PNG), report }
})
```

### `refine_cad_model` — critique → revised STL

```javascript
mcp__storyboard__refine_cad_model({
  source_model: "<openscad_source from the prior call>",   // or model_id
  critique: "base is too thin — make it 4mm and widen the foot to 80mm for stability; the lip should be 5mm so the phone can't slide off",
  // returns: a fresh { openscad_source, stl_url, render_url, report }
})
```

### `export_stl` — the validation gate

```javascript
mcp__storyboard__export_stl({
  source_model: "<openscad_source>",     // or model_id / stl_url
  validate: true,                         // run cad-validate before returning
  // returns: { stl_url, report: { is_watertight, is_winding_consistent, is_volume,
  //            volume_mm3, dimensions_mm:[x,y,z], min_wall_mm, triangles } }
})
// SHIP ONLY IF report.is_volume === true.
```

### Direct calls (pre-#430, or scripting / CLI)

```bash
SK="$YOUR_DAYDREAM_KEY"   # from ~/.daydream/credentials or your .mcp.json bearer

# 1. render OpenSCAD → STL.  Param key is `scad` (the source).
curl -s https://sdk.daydream.monster/inference \
  -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
  -d '{"capability":"cad-render","params":{
        "scad":"$fn=64; difference(){ /* …OpenSCAD source… */ }",
        "units":"mm"}}'
# → { "data": { "stl_url": "https://storage.googleapis.com/.../part.stl", "kind":"tool", ... } }

# 2. validate the STL (THE GATE).
curl -s https://sdk.daydream.monster/inference \
  -H "Authorization: Bearer $SK" -H "Content-Type: application/json" \
  -d '{"capability":"cad-validate","params":{"stl_url":"https://.../part.stl"}}'
# → { "data": { "metadata": { "n_vertices": 242, "n_faces": 488,
#       "is_watertight": true, "is_winding_consistent": true, "is_volume": true,
#       "euler_number": -2, "volume_mm3": 69085.4, "area_mm2": 26276.8,
#       "bbox_mm": [80.0, 126.75, 42.86] } } }
# SHIP ONLY IF is_volume === true.  (Cross-check locally with trimesh m.is_volume.)
```

## Tips (learned building the showcases)

- **The 4-view is your whole feedback signal.** Don't ship on the iso render alone — the *front* and *right* orthographic views are what reveal a non-flat base, a too-thin wall, or a floating island. Read them like a machinist reads a drawing.
- **Image-driven? Give it a clean reference.** A single object, low-poly or product-shot, neutral background, head-on or 3/4. `cad-render` reads silhouette and proportion — a cluttered photo with shadows and texture confuses the compile. Generate one with `flux-dev` ("low-poly desk hook, single object, white background, product render") if you don't have one.
- **Iterate — don't one-shot.** First render gets you 80% there; the 2nd–3rd refine pass is where walls thicken, the base flattens, and the fit dials in. Each `refine_cad_model` pass is cheap.
- **State dimensions in mm in your prompt.** "~70mm tall, 3mm walls, 60° lean" produces a far more printable first pass than "a phone stand." The model honors explicit mm.
- **Make the gate non-negotiable.** A part that's `is_volume: false` will *look* fine in the render and *fail* in the slicer. Validate before you celebrate. If it fails, the report names the defect — feed it straight into a refine pass.
- **Clamp to your bed.** Pass `max_dim_mm` for your printer (≤256 for most FDM). A model that's parametrically "20cm" is one number away from not fitting.

## Costs

| Operation | Cost | Notes |
|---|---|---|
| `create_cad_model` (one render) | ~$0.05–$0.15 | OpenSCAD compile + headless 4-view render |
| `refine_cad_model` (per pass) | ~$0.05–$0.15 | typically 1–3 passes to dial a part in |
| `export_stl` + `cad-validate` | ~$0.01 | trimesh load + report; cheap, run it every time |
| **Typical part, end to end** | **~$0.15–$0.45** | a functional, slicer-ready STL |

## See also

- **Showcase:** [`/chapters/text-image-to-3d-print-example.html`](/chapters/text-image-to-3d-print-example.html) — both STLs, their 4-view renders, the printability reports, and **working download links**.
- **Related capabilities:**
  - `tripo-i3d` / `triposplat` — learned organic mesh / Gaussian-splat (display, not print)
  - `flux-dev` / `gpt-image` — generate the clean reference image for image-driven parts
- **OpenSCAD** — https://openscad.org (the parametric language `cad-render` compiles to)
