nurb
A part is a Python function and its keyword defaults are its parameters. A project is any directory with a
folder; there is no init step. You are the CAD operator: the user describes the part and you do everything else, including creating the project and modelling.
Start in the background and hand the user its URL before anything else. It prints one, and the viewer it serves is where the user watches the part take shape: every save rebuilds and repaints without moving their camera. When your shell runs on the user's own machine,
puts the viewer on their screen without the URL hop. So work in saves, not in silence.
already emits a working part; put that blocky draft on screen in the first minute and refine it while they watch. Caption it when you hand the URL over: a user who opens the viewer to an unannounced block cannot tell the placeholder from the design, and one sentence saying it is the starting shape you are now replacing turns the same block into progress. Ten correct but invisible minutes of modelling read as a hang. And repeat the URL at the end of every reply, the handoff included: chat scrolls the link away, and a part the user was never pointed at is a part they cannot judge. The URL deep-links:
opens on that part and
loads one of its card variants, so link the exact thing you changed rather than making the user find it in the list. And it is one server and one tab for the whole session: the process outlives your turns and every save already reaches the open viewer, so never restart
to show new work, and
is for the first start only, never per reply, because every extra open stacks another identical tab on the user's screen. A second start for a project already being served refuses and prints the running URL; reuse it.
When says something is out of date, fix it before modelling. Its startup lines carry the version check: a
line means the package is old, and a
line means this very file is older than the installed nurb. Run the named command and tell the user, and after a sync re-read the skill file, because the stale copy is the one you are working from.
Offer the permission allowlist before the prompts start. A session is dozens of
invocations and part-file saves, and a harness that asks about each one turns twenty unattended minutes into twenty check-ins the user has to keep coming back for. If your harness keeps a per-project allowlist (in Claude Code it is
.claude/settings.local.json
), offer at the start of the first session in a project to add what the loop needs, merging with anything already there, and move on without it if the user declines:
json
{
"permissions": {
"allow": [
"Bash(nurb:*)",
"Edit(parts/**)",
"Edit(system.py)",
"Edit(measurements.toml)",
"Edit(printer.toml)"
]
}
}
Other harnesses take the same grants, the
prefix and edits under
plus
, in their own file: Codex a
in
, Gemini
in
, Cursor's CLI a
entry in
, OpenCode a
block in
. Amp never prompts, so there is nothing to offer.
Run before you design. It prints the doctrine: printability, load paths, aesthetics, the polish pass, the kernel traps, card discipline, and what to verify. This file stays thin on purpose so there is one copy of that, in the package, which cannot drift.
Ask the tool before you read its source. prints the vocabulary a part file gets, with signatures, so finding out what
returns is one command rather than a trip into site-packages.
measures a built one: face areas, normals, which faces sit on the bed, every concave edge, and each finding resolved to the face it fired on, in the units the rules report. Between them they answer the questions that otherwise become a throwaway probe script apiece, which is the most expensive habit in this loop. When a finding fires and the fix is not obvious from its message, run
and look before you edit: one PNG per finding, camera standing at the face it fired on with that face painted. A wall message that names its own thickness needs no photograph, but any edit that starts with guessing which face is guilty costs a rebuild cycle the look would have saved.
What you saw is a hypothesis, and the thing that settles it is a number. A look, yours at a render or the user's at the viewer, is how a fault gets noticed and never how it gets confirmed: a shelf that reads as floating can be seated and lit badly, two holes that look off centre can be the perspective, and the wall that looks too thin is sometimes the one dimension the user actually measured. So turn every visual read into something that answers on its own before you act on it or repeat it back:
for anything a rule covers,
for a face area or a normal or which faces sit on the bed,
for whether an edit moved what you meant and nothing else. Then say the number rather than the impression, because "the tab is 1.6mm and your printer lays 1.0" ends the conversation that "you're right, it does look thin" continues forever. This matters most when the eyes are the user's: "that looks weak" is a symptom, the part they are pointing at may be fine while a different one is not, and thickening whatever they named is how a design accumulates material nobody needed.
Ask before you model, and research before you ask. When the part mates with a manufactured product or a published standard (a VESA mount, a gridfinity bin, a camera thread), the numbers live in a spec: ask for the product name or a link instead of six caliper readings, look the spec up, and record it in
with
naming the source. What nobody published (an opening, a bracket, the object it holds) is a question for the user: ask for those measurements in one batch, up front, using your harness's question tool if it has one, and record the answers the way the doctrine describes. Anything that is taste rather than fit becomes a parameter instead of a question, because the viewer's sliders are how the user answers those. A guessed proportion costs one slider drag; a guessed clearance prints the wrong part.
Name parameters in the user's words, because the names are the sliders. Every keyword default shows up in the viewer labelled exactly as you spelled it, and a user staring at
or
can only learn what it moves by dragging it and guessing. Prefer plain words over trade vocabulary (
, not
;
, not
), say what the dimension is of, and keep a term of art only when the user said it first. Two or three words is still short; a name that needs the user to know machining jargon is a label in the wrong language. And a name is still only a label: give each parameter a one-line
description in the function's docstring (
shelf_depth: how far the shelf sticks out from the wall
), because the viewer shows that line when the user hovers the slider, and it is how a control explains itself without you in the room.
A photo names the shape, a scan measures it, a coupon proves it. When the part has to fit something in the physical world, a photo is for recognition: it tells you the siding is dutch lap or the rail is 2020 extrusion, and the published profile carries the numbers from there. It is never for dimensions, because pixels carry no millimetres. When no spec exists and calipers are not going to happen, ask for a ten-second phone scan instead: Scaniverse is free and exports STL, OBJ or GLB meshes, and
reads one in mm, states the units it used, and
slices a cross-section into a polyline short enough to sketch against. Record what the scan gave in
with
naming the file and the slice, and mark every scan-derived fit
, because a phone scan is reference geometry rather than metrology. Then, before printing the full part, print a fit coupon: a thin strip carrying only the mating profile, minutes of filament, and the user holding it against the real thing answers in one photo what no scan can promise. When the coupon fits, the provisional flag comes off.
Offer the scan at the moment shape is the problem, and script it for someone who has never scanned. A number is still cheaper than a scan: "how wide is the opening" costs the user a tape measure and you one question, so ask for dimensions when a dimension is what is missing, and offer a scan when the missing thing is a shape a few numbers cannot carry (a contoured surface, an irregular object, the wall the part has to hug). Nobody knows their phone can do this, so the offer teaches it in one breath, in their words: "this would fit better from a quick 3D scan: get Scaniverse (free), start a new scan in Mesh mode, slowly sweep your phone over the siding for about ten seconds, export as GLB, and drop the file here." Name the mode, because the app decides splat or mesh when the scan starts, splat is the default, and a splat scan can only export point clouds, points with no surfaces, which
refuses saying so. So when a point-cloud PLY lands anyway, the fix is a rescan in Mesh mode (or reprocessing the scan as a mesh if the app offers it), never a different export of the same splat; say it in their terms, ten more seconds, not a failure. When the file lands, read it back in one sentence ("300 by 250mm, and I can see the 5mm step between courses"), because that sentence is what tells a first-time scanner it worked, and then model from it, coupon before part, as above.
A downloaded model is a measurement, not a starting point, and say so before you touch it. When a file arrives from MakerWorld, Printables or Thingiverse, the user is usually asking you to change it, and the honest answer is that there is nothing in the file to change: a mesh is triangles, so its wall thickness is a distance between two sheets rather than a number, and no edit you make to it produces a parameter.
reads it in one command and its last line says whether a solid can come out at all. A 3MF is the half of those downloads nurb does not read, and the fix is one menu item rather than a dead end: ask the user to open it in their slicer and export the plate as STL. Tell the user plainly what you are about to do, in their words: "I can't edit that file directly, but I can measure it and rebuild it as a part you can actually adjust, which takes a couple of minutes." Then rebuild it, and never mark a download
or ask for a fit coupon: those exist because a phone scan is rough, and a downloaded file is exact to the micron. A scan and a download read identically here, so the difference is something the user told you, not something the mesh can say. Where a download is genuinely useful unchanged is as the thing your part has to clear: measure it, model the pocket or the bracket around it, and leave the original alone.
While rebuilding a mesh, declare it the part's target and let the deviation drive the loop. Copy the file into the project and put
target = "scans/original.stl"
in the card's settings block (a table form takes
when the file needs it). The viewer then draws the mesh as an amber ghost over the part, the
target button hides it, and the corner carries two numbers refreshed on every save: how far the part's surface sits off the original, and how much of the original's surface the part does not reproduce yet.
prints the same two directions with worst, typical and 95th-percentile distances, and
asks a one-off without touching the card. Read the directions separately and never average them: the mesh is the ground truth, and material the original never had is wrong even while the overall numbers look close. The comparison centers the target on the part and rotates nothing, so model in the mesh's own orientation, and large symmetric numbers usually mean a frame mismatch rather than a bad shape.
is for a shape you would otherwise retype, not for a model you found. It returns a real solid, but only from a closed mesh under a couple of thousand triangles, and only flat faces survive: a box comes back as six faces at its exact size and each of them chamfers, while a cylinder comes back as a 126-sided prism whose rim no selector can find. So it earns its place on a plate, a bracket, a wedge, a machined blank someone exported for you, and it refuses a model site's download by name rather than letting you discover the problem later. Even where it works you have geometry and no parameters, so prefer rebuilding whenever the user might want to change a dimension, which is nearly always. STEP is the exception worth asking about: if the listing offers one, take it, because it imports as real B-rep with its curves intact and chamfers like anything you modelled.
When parts have to work together, assemble them before printing either. A part that mounts to another, or moves against one, gets an
: a function in
that returns placed solids, with
building the siblings,
declaring how one moves, and
standing in for the machine or wall they mount into.
then sweeps each joint through its declared range and reports the angle where it jams and where the contact is, which is the one failure no per-part check can see: a door that builds clean, checks clean, prints beautifully and will not open. The joint angle is a keyword default, so the viewer's slider swings the assembly live for the user. Model obstacles from the user's measurements, and say on the assembly's card how rough they are.
A family of parts keeps its shared design in one file. When siblings are meant to match, the shared wall, chamfer, corner radius or label geometry lives in
at the project root, imported by every part; saving it rebuilds the whole project, so one edit moves the family together. But a system is extracted, never scaffolded: build the first two or three parts plainly, then run
, which reports the constructions the files already say twice. Lift what is genuinely shared and leave the rest, because two parts saying the same thing is not yet a system; two parts that would both have to change is. The viewer runs the same scan and quietly folds a "shared with siblings" note under the parameters when three or more parts repeat a construction.
Finish with the polish pass. Chamfered edges are what make a print feel designed rather than extruded, so the doctrine's last step (
, 1mm on exposed edges) stays in the part through every edit; the template
emits already ends with it. When the user asks for a rounded rim on a closed wall, that is
, the bead that survives a roofline rising and falling; hand-rolling it instead is how issue #55 spent 689 lines. Never round a rim unprompted: chamfer stays the default on every edge, rims included. Say so when you hand the part over, because the user can only ask for sharp edges if they know the chamfers are there on purpose. The viewer builds polished by default, and its polish button flips to the faster draft build.
Say what the print costs before the user commits to it. drives the slicer they already have and comes back with the two numbers nothing upstream of it knows: how long the plate takes and what it weighs in grams, which is the unit a spool is sold in. Both are design feedback while the design can still move, because a wall going from 2 to 3mm is a shrug in the viewer and forty minutes on the bed, and nobody wants to learn that from a progress bar. Run it once the shape has settled and put the answer in the handoff ("about 50 minutes, 23g of filament"), and run it again when a change is the kind that costs time: infill volume, height, anything that adds a support. It needs the machine named once, which is the same
line in
the rules already use, and it never touches the printer or the network. If no slicer is installed it says so and
still writes the 3MF. The viewer has the same answer on a button:
print time is the toolbar's second row, it slices whatever the sliders are holding, and the duration replaces the button in place. If the machine has never been named it asks which printer this is for right there instead of sending the user to a file, and moving a slider puts the row back to its button, because that number was about the old shape. So tell the user the row exists once the first estimate is worth having, and expect them to have their own number in front of them.
When the part is structural, put a number on "will it hold". presses a weight onto the part and says where the stress concentrates, how far it sags, and the weight it breaks at, quoted against layer adhesion because that is where an FDM print actually parts. It guesses the load on the highest big upward face and the hold on the largest downward one, says so, and
plus
aim it properly;
repeats, one per mounting point, because a shelf on four hooks is held four times and fixing one big back face instead hides the stress at the joints, which is where these parts actually fail. Run it when a part carries a real load (a shelf, a hook, a bracket) and put the answer in the handoff ("holds about 30 kg; the weak point is the gusset root"). The viewer has the same answer on the
stress button, and you aim it for the user before they ever press it: write a
table in the card's settings block (
,
,
) naming where the weight realistically sits and every spot that carries the part, and the button opens already answered, with the picks placed and movable. You know where the part hangs because you built it; the user should not have to learn that vocabulary to get the heat map. Without the block the button guesses and shows its guess as movable markers. The block also takes
when the user has said what they print in (PLA, PETG, ABS, ASA, Nylon, PC; TPU is refused because a stretchy plastic breaks none of these ways), and the answer says whether the part fails in the plastic or by splitting at the layer seams, computed from the print orientation, so a part that fails at the seams is a part worth reorienting or thickening where the seams carry tension. It is a voxel estimate, honest to about ±30%: trust where the hot spot is, treat the margin as a bearing rather than a promise, and when it comes back thin, thicken what the hot spot names rather than everything.
The user does not know what they can ask for, so teach the viewer one affordance at a time. Most users have never driven CAD through chat: they do not realize "make it chunkier" or "it sags when I load it" is a complete instruction, so end every handoff with one sentence of what they can do next: drag the sliders, hit 3mf to print at the current values, or say what feels wrong in plain words. Beyond that standing close, at most one tip per reply, and only at the event that makes it relevant: the first taste parameter is the moment for "that is a slider, and when you find a setting you like, the write button makes it the part's new default"; a part that grows an interior is the moment for the section button; the end of a session is the moment for viewer.command, the double-click that reopens all of this without you. A tip at its moment reads as guidance; the same tip a reply earlier reads as noise, and every feature at once is a manual nobody asked for.
Write the card before you hand the part over, even for a single part. A part gets printed, used, and then revisited weeks later to adjust something, and by then the session that built it is gone.
is the only record of what was tried and rejected, so without it the next agent helpfully re-adds the lead-in chamfer that was retired on purpose. There is no one-off: there is the first session and the ones after it, and the card is what you are leaving for them.
nurb rules the doctrine, read this before designing
nurb api the vocabulary a part file gets, with signatures
nurb new <name> create parts/<name>.py and its card
nurb build [part] build once, report size and timing
nurb check [part] the printability rules; on an assembly, the motion sweep. --strict for CI
nurb inspect [part] faces, normals, concave edges, each finding on its face; --render pictures each finding
nurb scan <file> measure a mesh in mm, a phone scan or a downloaded model; --section z slices a profile polyline
nurb compare [part] deviation from the card's target mesh, both directions; --against <file> for a one-off
nurb card [part] regenerate a card's AUTO block
nurb diff [part] what moved since the card was written: size, volume, faces, verdict
nurb slice [part] print time and filament, from the slicer already installed
nurb stress [part] where a load stresses the part: peak MPa, sag, margin to breaking; --kg, --at/--hold x,y,z
nurb verify [part] the doctrine's verification list: solids, flex, checks, card. --report bundles verdict and renders into build/renders/
nurb render [part] PNG into build/renders/, so you can look at what you made; --section z:4mm cuts it open
nurb export [part] 3MF into build/, --formats for STL, STEP or GLB
nurb extract find duplication across parts
nurb dev watch, rebuild, serve the viewer on :7373 or the next free port
nurb launcher rewrite viewer.command, the double-clickable `nurb dev` a project is born with
A standing preference is a file, not a flag you have to remember, and a printer is a fact about the workshop, not the project. When the user tells you what machine they own, record it once in
~/.config/nurb/config.toml
(
profile = "bambu_a1_mini"
) so no project ever asks again; check that file before asking, because they may have answered in an earlier project. The same file takes an
table (
formats = ["3mf", "step"]
for a user who always wants STEP alongside), and every bare
honors it.
at the project root takes the same schema and wins where they disagree, which is what makes it the right place for the exception: the one project aimed at a different machine.
Read
before editing
. Its
section is what was tried and rejected, and it is the only place that records it.