Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

textcad

Natural language → OpenSCAD parametric CAD, via a fully-local agentic loop.

ci license: MIT python

textcad-generated hex nut, turntable

Describe a part in plain English; a local LLM writes parametric OpenSCAD; OpenSCAD renders and exports it; and compile, STL-export, and local vision-model gates feed any failure back to the model for repair — until the part is correct.

No cloud, no API keys, no telemetry. Codegen runs on a local Ollama model; rendering uses the OpenSCAD binary as an external tool.

"a hexagonal nut, 16mm across flats, 6mm thick, 8mm center hole"
   → LLM writes parametric OpenSCAD          (Ollama, e.g. qwen2.5-coder:32b)
   → OpenSCAD renders iso + top views, exports STL
   → [compile gate] [STL-export gate] [vision-model gate]
   → a failed gate feeds specific feedback back → the model rewrites
   → ✓ correct part

Self-correction in action

On the hex nut above, the model's first attempt was the wrong shape. The vision gate — judging a top-down orthographic view (examples/hexnut_closedloop_top.png), because a foreshortened isometric makes small vision models misread a hexagon as "rectangular" — rejected it with "not hexagonal; irregular polygonal shape". That critique fed back, and attempt 2 was approved. No human in the loop.

Multi-view inspection

The vision gate judges an orthographic contact sheet — one labelled image, like an engineering drawing — so features on different faces are all legible at once:

contact sheet

Gallery

All generated locally from a one-line description by qwen2.5-coder:32b:

Hex nut Washer Standoff Cube + bore Pulley L-bracket

The L-bracket is a multi-feature part — two perpendicular legs and a bolt hole through the flat face of each. Getting it right needed the gate work below.

Install

git clone https://github.com/dbhavery/textcad
cd textcad
pip install -e .

# prerequisites
#  1. Ollama running locally with a codegen model:   ollama pull qwen2.5-coder:32b
#  2. (optional, for --inspect) a vision model:        ollama pull qwen2.5vl:7b
#  3. an OpenSCAD binary on PATH, or set $TEXTCAD_OPENSCAD,
#     or drop a portable build under tools/openscad-*/

Usage

# compile-only loop (fast; accepts the first part that compiles and exports)
textcad "a flat washer, 24mm outer, 10mm hole, 2.5mm thick" --model qwen2.5-coder:32b

# full closed loop with the local vision inspector
textcad "a hexagonal nut, 16mm across flats, 6mm thick, 8mm center hole" \
    --model qwen2.5-coder:32b --inspect qwen2.5vl:7b --iters 4

# compare codegen models on a fixed part set
python scripts/bench.py qwen2.5-coder:32b

Outputs: out/<name>.scad, out/<name>.png (iso), out/<name>_top.png (top-down, for the inspector), out/<name>.stl.

Codegen backends (local-first, optional frontier)

textcad is local by default (--backend ollama). For hard multi-feature parts you can opt into a frontier model via its subscription CLI — no API key:

textcad "an L-bracket, 2 perpendicular legs, a 5mm hole in each leg" --backend claude
textcad "..." --backend codex            # OpenAI Codex CLI

The local 32B coder needs heavy prompting for multi-feature geometry and still slips (~2/3 correct); a frontier backend places it correctly zero-shot, one attempt. Backends run from a neutral temp dir so the agent doesn't inherit your project context. Default stays Ollama — nothing leaves your machine unless you ask for it.

The three gates

Each iteration runs the gates in order; a failure feeds targeted text back to the next generation:

  1. Compile — OpenSCAD must render a preview; the compiler stderr feeds back.
  2. STL export — must produce a non-empty STL. Catches mixed 2D/3D and non-manifold geometry that previews fine but won't export.
  3. Visual inspector (optional, --inspect) — a local VLM judges the part against the request and rejects valid-but-wrong shapes (a round disc when a hexagon was asked for) that the first two gates can't see. It inspects an orthographic contact sheet — top, front and side views composited into one labelled image (like an engineering drawing). This matters: a small VLM misreads a foreshortened isometric, and gets confused by several separate images, but reads one labelled multi-panel sheet reliably — enough to catch a missing perpendicular leg or a wrong cross-section. (Contact sheets need Pillow: pip install textcad[inspect].)

Library API

from textcad import run, make_vlm_inspector

result = run(
    "a hexagonal nut, 16mm across flats, 6mm thick, 8mm center hole",
    model="qwen2.5-coder:32b",
    inspector=make_vlm_inspector("qwen2.5vl:7b"),  # omit for compile-only
    iters=4,
)
print(result["stl"], result["attempts"])

Every layer takes an injectable backend (llm=, inspector=), so the loop is unit-tested with no Ollama and no OpenSCAD — see tests/.

Layout

textcad/
  llm.py        minimal local-Ollama client (text + vision, multi-image)
  codegen.py    system prompt + OpenSCAD code generation
  render.py     OpenSCAD invocation: iso/ortho views + STL + contact sheet
  inspector.py  local vision-model judge (orthographic contact sheet)
  loop.py       the agentic loop + the three gates
  cli.py        `textcad` command
scripts/bench.py  fixed-part-set model comparison
tests/            mock-backed gate-logic + codegen + inspector tests

What works, what doesn't (honest findings)

  • The closed loop is verified end-to-end and fully local — single-feature parts (hex nut, washer, standoff, cube+bore) and now a multi-feature L-bracket (two perpendicular legs, a bolt hole through each leg's flat face).
  • Multi-feature generation needed prompt engineering + a helper, not a bigger model. A smaller qwen3:14b makes wedges and skips difference(); qwen2.5-coder:32b does the geometry but originally botched multi-body parts by assembling with center=true+rotate (disconnected "plus" shapes) and ran bolt holes along a plate's length. Fixes, in order of impact:
    1. corner-based placement rule + a worked L-bracket example → reliable gross shape (a real L, not a plus);
    2. a plan-in-comments step so the model reasons about coordinates first;
    3. an injected thru_hole(pos, axis, d) helper prepended to every file — the model calls it and only chooses the axis (the plate's thin dimension) and a centred position, so it can't get the error-prone rotate/length/center mechanics wrong. This took the both-holes-correct rate from ~1/3 to ~2/3. At ~2/3 correct per attempt, the agentic loop reliably converges on a correct part.
  • Inspection is solved for multi-feature parts via the orthographic contact sheet: where a 7B VLM failed on both a single isometric and a raw 3-image call, it correctly approves a real L-bracket and rejects a flat L-plate from one labelled contact sheet.
  • A larger VLM (qwen2.5vl:32b) is unsupported on the current Ollama (0.30.10 is the latest; it fails to load that model's vision encoder) — but the contact-sheet trick made the small 7B sufficient, so it isn't required.

Still-open edge: the coder's last weak spot is which axis the upright-plate hole takes (~1 in 3 swaps it); the helper fixes the mechanics, not that semantic choice. Next steps: more part archetypes in the prompt (or a template library), and a parameter-slider UI over the named OpenSCAD vars.

License & provenance

MIT — see LICENSE. textcad is a clean-room project: it reproduces a general pattern (LLM writes CAD code → render → inspect → iterate) but contains no third-party source. The OpenSCAD binary is invoked as an external tool, so textcad is an independent work and inherits no GPL obligations.

About

Natural language to OpenSCAD parametric CAD via a fully-local agentic loop (Ollama codegen + vision-model inspector). Clean-room, MIT.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages