Skip to main content

nurb

Agentic CAD for 3D printing. The user is a language model: your agent writes parts as Python functions, nurb builds them into real solids, checks them against print physics, and shows you the result live. You judge, drag sliders, download the STL.

Install

uv tool install nurb       # or: pip install nurb

Teach your agent

Install the nurb skill once and your agent reaches for nurb on its own whenever you ask for a printable part. skills.sh detects whatever harnesses you have and installs into each:

npx skills add shpigford/nurb

Or nurb skill prints the same skill file as plain markdown, for redirecting wherever your harness reads instructions:

mkdir -p ~/.claude/skills/nurb && nurb skill > ~/.claude/skills/nurb/SKILL.md   # Claude Code
nurb skill >> AGENTS.md                                # or any harness that reads AGENTS.md

Make something

Open your agent in the directory where the project should live, and talk:

Make me a phone stand: 15 degree lean, a slot for the charging cable, fits a phone 78mm wide.

The agent does the rest: reads the design doctrine, creates the project, models the part, runs the printability checks, and starts nurb dev so you get a link to watch. Every save updates the browser without moving your camera, and check findings pin themselves to the geometry. When it looks right: drag the sliders if you want, click stl, print. A write button saves your slider values back into the file's defaults, where the agent will see them.

A part

from nurb import *

@part
def phone_stand(width=84.0, lean=15.0, wall=3.0, draft=False):
    ...

The keyword defaults are the parameters. That one declaration drives the CLI, the viewer's sliders, and the tests; there is no schema to keep in sync. A float is a dimension, an int is a count (its slider steps by one). draft is passed by the runtime, not callers: when true, skip the polish pass.

The body of a part is build123d code on the OCCT kernel. That is why the solids are real B-reps with working chamfers, fillets, and STEP export rather than meshes, and why your model already knows the modelling API.

Commands

nurb new <name>     create parts/<name>.py and its card
nurb dev            watch, rebuild, serve the viewer
nurb build [part]   build once and report size
nurb check [part]   run the printability rules, --strict for CI
nurb rules          print the design doctrine
nurb skill          print an agent skill file for your AI harness
nurb card [part]    regenerate a card's AUTO block
nurb verify [part]  run the doctrine's verification list
nurb render [part]  write a PNG into build/
nurb export [part]  write STL and STEP into build/, --formats for GLB
nurb extract        find duplication across parts

A project is any directory with a parts/ folder. No init step.

Checks

The agent cannot see, so nurb check is its eyes. Rules run against the exact solid, not a mesh, and findings come back as text with coordinates:

overhang          downward faces past 45 degrees, bridges told from cantilevers
min_wall          thinnest section, ray cast corrected by an inscribed sphere
sliver            faces too small to print as anything but a smear
concave_cosmetic  polish laid into an inside corner
bed_bevel         polish laid on the edges that meet the build plate
stability         center of mass outside the footprint
projection_ratio  reach over height, for a part cantilevered off a wall
build_volume      does it fit the printer at all

Name your machine once in printer.toml (profile = "bambu_a1_mini"), or try another with nurb check --printer prusa_mk4s. A part records what it has already justified on its card, so known findings stay silent and new ones are regressions:

[part]
min_wall = 1.0

[accepted]
sliver = 6

When text is not enough, nurb render <part> screenshots the viewer so the agent can look at its own work (uv sync --extra render && uv run playwright install chromium, the only part of nurb that wants a browser).

Cards and measurements

Agents forget everything between sessions, so each part gets a card (parts/<name>.md): what it is, why, and a ## Don't section for what was tried and rejected. nurb card regenerates its one machine-written block with what only a build can tell you.

Dimensions an agent cannot derive go in measurements.toml with how they were obtained; measured("bracket_pitch") returns them, and asking for one that is not there raises instead of letting the model guess. A guessed dimension builds, checks clean, prints, and does not fit.

Variants

The same function flexed is a variant on the card, not a copy of the file:

[variants.shelf_3x2.params]
grid_x = 3

build, check, card and export walk variants like parts, so each gets its own STL and baselines.

Layout

parts/<name>.py     the part
parts/<name>.md     its card
system.py           optional: shared constants and geometry
measurements.toml   optional: real-world dimensions with provenance
printer.toml        optional: which machine this project prints on
build/              generated, gitignored

Speed

nurb dev is a long-lived process because importing the kernel costs 45s cold. After that, rebuilds run 30 to 400ms depending on the part, which matters because an agent iterates in save-check cycles, dozens per part.

Tests

uv run pytest. The parts in examples/ are the calibration set, asserted against dimensions from really-printed parts. Fit tests use literal numbers, never the part's own constants: a model's tests love to agree with its code.

Not built yet

  • A hosted configurator. nurb dev already is one for anybody who can reach it, but publishing without a running kernel is a different problem.
  • Measurement tools in the viewer.
  • min_wall probes sample faces, so a pinch nothing lands near is still missed. A clean result means "no thin walls found", not "no thin walls".

Debugging the viewer

window.__nurb exposes { THREE, scene, camera, controls, mesh, ready }. The URL takes ?part=<name>, ?view=iso|front|back|left|right|top, and ?bare. three.js is vendored in src/nurb/vendor/three, so the viewer needs no network; see the README beside it before changing versions.

License

FSL-1.1-MIT. Source-available for any purpose except building a competing product, and converts to plain MIT two years after each release.

Copyright 2026 Ordinary Systems LLC.

Third-party notices

nurb uses Open CASCADE Technology (OCCT) for all B-rep geometry, reached through build123d (Apache-2.0) and the OCP bindings (Apache-2.0). OCCT is licensed under LGPL-2.1 with an additional exception. nurb does not redistribute OCCT; it is installed as a dependency and dynamically linked. Bundling nurb into a single-file distribution that embeds OCCT would require shipping the OCCT license and keeping the library replaceable, per LGPL.

nurb does redistribute three.js r169 (MIT), vendored so the viewer works offline, with its LICENSE beside it.

Other dependencies: trimesh (MIT), watchdog (Apache-2.0), websockets (BSD-3-Clause), numpy (BSD-3-Clause). Optional, for nurb render only: playwright (Apache-2.0).

npm note: nurb has no JavaScript to ship, so PyPI is the only install channel. @shpigford/nurb just points npx users here.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nurb-0.1.1.tar.gz (278.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nurb-0.1.1-py3-none-any.whl (289.7 kB view details)

Uploaded Python 3

File details

Details for the file nurb-0.1.1.tar.gz.

File metadata

  • Download URL: nurb-0.1.1.tar.gz
  • Upload date:
  • Size: 278.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nurb-0.1.1.tar.gz
Algorithm Hash digest
SHA256 c8325a7e891ec10192566573e4fcc5b096902bb7dc1b1dc453139cd267957267
MD5 4d4902d4ea0990ed0836bf85d0243c97
BLAKE2b-256 ba2815f0b55bd1ad406fb7ee5e4bf20f5ae6d63912543c0787387643b785fd6c

See more details on using hashes here.

File details

Details for the file nurb-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: nurb-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 289.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.32 {"installer":{"name":"uv","version":"0.11.32","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for nurb-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3a01ce81447daa84ae0bcc317eb6924a7aa7e8809475fcd98d82c1f4360c4c8a
MD5 2965f3fd68f745f9c6bca9cbd017ae45
BLAKE2b-256 3808a97f79f8cde40f7ed24685a4fb8ac17c76733ded272a51ce41649027f8b0

See more details on using hashes here.

Release history Release notifications | RSS feed

0.23.0

2 files

0.22.2

2 files

0.22.1

2 files

0.22.0

2 files

0.21.0

2 files

0.20.0

2 files

0.19.2

2 files

0.19.1

2 files

0.19.0

2 files

0.18.2

2 files

0.18.1

2 files

0.18.0

2 files

0.17.0

2 files

0.16.0

2 files

0.15.1

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

This release

0.1.1 This release

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page