nurb 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nurb-0.1.0/LICENSE +110 -0
- nurb-0.1.0/PKG-INFO +302 -0
- nurb-0.1.0/README.md +280 -0
- nurb-0.1.0/pyproject.toml +48 -0
- nurb-0.1.0/src/nurb/__init__.py +36 -0
- nurb-0.1.0/src/nurb/agents.md +23 -0
- nurb-0.1.0/src/nurb/builder.py +216 -0
- nurb-0.1.0/src/nurb/card.py +148 -0
- nurb-0.1.0/src/nurb/checks.py +790 -0
- nurb-0.1.0/src/nurb/cli.py +542 -0
- nurb-0.1.0/src/nurb/doctrine.md +315 -0
- nurb-0.1.0/src/nurb/edit.py +123 -0
- nurb-0.1.0/src/nurb/extract.py +151 -0
- nurb-0.1.0/src/nurb/measurements.py +125 -0
- nurb-0.1.0/src/nurb/polish.py +69 -0
- nurb-0.1.0/src/nurb/printers.toml +35 -0
- nurb-0.1.0/src/nurb/py.typed +0 -0
- nurb-0.1.0/src/nurb/registry.py +33 -0
- nurb-0.1.0/src/nurb/render.py +128 -0
- nurb-0.1.0/src/nurb/server.py +448 -0
- nurb-0.1.0/src/nurb/vendor/three/LICENSE +21 -0
- nurb-0.1.0/src/nurb/vendor/three/README.md +30 -0
- nurb-0.1.0/src/nurb/vendor/three/addons/controls/OrbitControls.js +1523 -0
- nurb-0.1.0/src/nurb/vendor/three/addons/loaders/GLTFLoader.js +4725 -0
- nurb-0.1.0/src/nurb/vendor/three/addons/utils/BufferGeometryUtils.js +1373 -0
- nurb-0.1.0/src/nurb/vendor/three/build/three.module.min.js +6 -0
- nurb-0.1.0/src/nurb/viewer.html +904 -0
nurb-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Ordinary Systems LLC
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the MIT license that is effective on the second anniversary of the date we make
|
|
91
|
+
the Software available. On or after that date, you may use the Software under
|
|
92
|
+
the MIT license, in which case the following will apply:
|
|
93
|
+
|
|
94
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
95
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
96
|
+
the Software without restriction, including without limitation the rights to
|
|
97
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
98
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
99
|
+
so, subject to the following conditions:
|
|
100
|
+
|
|
101
|
+
The above copyright notice and this permission notice shall be included in all
|
|
102
|
+
copies or substantial portions of the Software.
|
|
103
|
+
|
|
104
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
105
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
106
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
107
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
108
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
109
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
110
|
+
SOFTWARE.
|
nurb-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nurb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agentic CAD for 3D printing
|
|
5
|
+
Keywords: cad,3d-printing,parametric,build123d,occt
|
|
6
|
+
Author: Josh Pigford
|
|
7
|
+
Author-email: Josh Pigford <josh@joshpigford.com>
|
|
8
|
+
License-Expression: LicenseRef-FSL-1.1-MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Manufacturing
|
|
12
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: build123d>=0.11.1
|
|
15
|
+
Requires-Dist: trimesh>=4.12.2
|
|
16
|
+
Requires-Dist: watchdog>=6.0.0
|
|
17
|
+
Requires-Dist: websockets>=16.1.1
|
|
18
|
+
Requires-Dist: playwright>=1.49 ; extra == 'render'
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Provides-Extra: render
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# nurb
|
|
24
|
+
|
|
25
|
+
Agentic CAD for 3D printing.
|
|
26
|
+
|
|
27
|
+
A part is a Python function. Its keyword defaults are its parameters. `nurb dev`
|
|
28
|
+
watches your parts, rebuilds them on save, and pushes new geometry to a browser
|
|
29
|
+
without moving your camera.
|
|
30
|
+
|
|
31
|
+
Built on [build123d](https://build123d.readthedocs.io) (OCCT), so parts are real
|
|
32
|
+
B-rep solids with working chamfers, fillets, and STEP export.
|
|
33
|
+
|
|
34
|
+
## Try it
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv run nurb new dispenser
|
|
38
|
+
uv run nurb dev # http://127.0.0.1:7373, or the next free port
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Edit `parts/dispenser.py` and watch it update.
|
|
42
|
+
|
|
43
|
+
## A part
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from nurb import *
|
|
47
|
+
|
|
48
|
+
@part
|
|
49
|
+
def dispenser(width=80.0, height=120.0, wall=2.0, draft=False):
|
|
50
|
+
body = Box(width, height, wall)
|
|
51
|
+
if draft:
|
|
52
|
+
return body
|
|
53
|
+
bed = body.bounding_box().min.Z
|
|
54
|
+
keep = body.edges().filter_by(lambda e: e.bounding_box().min.Z > bed)
|
|
55
|
+
return polish(body, keep, 1.0)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`draft` is optional and passed by the runtime, not the caller. When it's true the
|
|
59
|
+
part should skip its polish pass. `nurb dev` builds in draft by default: on this
|
|
60
|
+
trivial part it's 18ms polished vs 1ms draft, and on a real one the saving is
|
|
61
|
+
nearer 20%.
|
|
62
|
+
|
|
63
|
+
## Commands
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
nurb new <name> create parts/<name>.py and its card
|
|
67
|
+
nurb dev watch, rebuild, serve the viewer
|
|
68
|
+
nurb build [part] build once and report size
|
|
69
|
+
nurb check [part] run the printability rules
|
|
70
|
+
nurb rules print the design doctrine
|
|
71
|
+
nurb card [part] regenerate a card's AUTO block
|
|
72
|
+
nurb verify [part] run the doctrine's verification list
|
|
73
|
+
nurb render [part] write a PNG into build/
|
|
74
|
+
nurb export [part] write STL and STEP into build/, --formats for GLB
|
|
75
|
+
nurb extract find duplication across parts
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A project is any directory with a `parts/` folder. There's no init step, and there's
|
|
79
|
+
no such thing as being outside a project: `mkdir -p thing/parts && cd thing && nurb new
|
|
80
|
+
clip` is the whole setup for a one-off.
|
|
81
|
+
|
|
82
|
+
`nurb dev` serves one project, so two projects means two of them. It takes 7373 if that
|
|
83
|
+
is free and walks up if it is not, printing where it landed, and the sidebar and the
|
|
84
|
+
browser tab both carry the project name so two of them are not mistakable for each
|
|
85
|
+
other.
|
|
86
|
+
|
|
87
|
+
Names are deliberately boring. The primary user is a language model, and a model that
|
|
88
|
+
has never seen this tool can guess `build`, `check` and `export`. It cannot guess a
|
|
89
|
+
themed alias.
|
|
90
|
+
|
|
91
|
+
## Why a long-lived process
|
|
92
|
+
|
|
93
|
+
Importing build123d costs 45s cold and 2.3s warm, and that is the whole argument: the
|
|
94
|
+
dev server pays it once instead of on every save.
|
|
95
|
+
|
|
96
|
+
What a rebuild costs after that depends on the part. A simple one is 29ms to build and
|
|
97
|
+
1ms to tessellate. The heaviest part in `examples/` is 401ms and 30ms. Draft mode is
|
|
98
|
+
not the lever it looks like: chamfers are 23% of that build, not most of it.
|
|
99
|
+
|
|
100
|
+
Tessellation used to be the larger half, at 620ms on that part, and almost none of it
|
|
101
|
+
was geometry. `Shape.tessellate` reads its triangles with `for t in poly.Triangles()`,
|
|
102
|
+
and OCP's iterator over that array costs 536ms where reading the same 7790 triangles by
|
|
103
|
+
index costs 6.8ms. `builder._triangulate` does the latter and returns bit-identical
|
|
104
|
+
vertices and faces. It is worth knowing before optimising the wrong thing.
|
|
105
|
+
|
|
106
|
+
## Layout
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
parts/<name>.py the part
|
|
110
|
+
parts/<name>.md its card: what it is, why, what not to retry
|
|
111
|
+
system.py optional: shared constants and geometry, importable from a part
|
|
112
|
+
measurements.toml optional: real-world dimensions with how they were obtained
|
|
113
|
+
printer.toml optional: which machine this project prints on
|
|
114
|
+
build/ generated, gitignored
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Cards are colocated with parts and share a basename. That's the whole link; a
|
|
118
|
+
rename is `git mv` on two files.
|
|
119
|
+
|
|
120
|
+
## Checks
|
|
121
|
+
|
|
122
|
+
`nurb check` runs the printability rules against the solid rather than an exported
|
|
123
|
+
mesh, so it sees real faces with exact areas and normals instead of triangles.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
overhang downward faces past 45 degrees, bridges told from cantilevers
|
|
127
|
+
min_wall thinnest section, ray cast corrected by an inscribed sphere
|
|
128
|
+
sliver faces too small to print as anything but a smear
|
|
129
|
+
concave_cosmetic polish laid into an inside corner
|
|
130
|
+
bed_bevel polish laid on the edges that meet the build plate
|
|
131
|
+
stability center of mass outside the footprint
|
|
132
|
+
projection_ratio reach over height, for a part cantilevered off a wall
|
|
133
|
+
build_volume does it fit the printer at all
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`min_wall`'s ray is exact on flat parallel walls and measures the slant through a skewed
|
|
137
|
+
one, so any chord thin enough to change the verdict is corrected by the largest sphere
|
|
138
|
+
tangent at that point, computed against the solid with exact kernel distances. A sphere
|
|
139
|
+
whose far contact is a graze rather than a wall is rejected by the same 0.3 cosine floor
|
|
140
|
+
the ray's exit filter uses, which is what keeps a detent dimple's bowl from reading as a
|
|
141
|
+
thin section of the web it is pressed into.
|
|
142
|
+
|
|
143
|
+
The bed size belongs to the machine, not to a part, so it is not written on cards.
|
|
144
|
+
A project picks a shipped profile once, in `printer.toml` at the root:
|
|
145
|
+
|
|
146
|
+
```toml
|
|
147
|
+
profile = "bambu_a1_mini"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Any check setting can be overridden in the same file, machine-wide. A card still wins
|
|
151
|
+
for what its part has justified. `nurb check --printer prusa_mk4s` answers "does this
|
|
152
|
+
fit that machine" without touching the file, and naming a profile that does not exist
|
|
153
|
+
lists the ones that do.
|
|
154
|
+
|
|
155
|
+
Every part carries what it has already justified on its card, so a known finding is
|
|
156
|
+
silent and a new one is a regression:
|
|
157
|
+
|
|
158
|
+
```toml
|
|
159
|
+
[part]
|
|
160
|
+
min_wall = 1.0
|
|
161
|
+
|
|
162
|
+
[accepted]
|
|
163
|
+
sliver = 6
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
It reports by default and takes `--strict` for CI, on the grounds that a warning which
|
|
167
|
+
blocks work gets switched off. Findings also show up in `nurb dev`, with a pin on the
|
|
168
|
+
geometry at each one.
|
|
169
|
+
|
|
170
|
+
## Variants
|
|
171
|
+
|
|
172
|
+
Some parts in a catalog are the same function flexed rather than new geometry. Those
|
|
173
|
+
ship as variants on the card, not as copies of the file:
|
|
174
|
+
|
|
175
|
+
```toml
|
|
176
|
+
[variants.shelf_gridfinity_3x2.params]
|
|
177
|
+
grid_x = 3
|
|
178
|
+
bracket_count = 6
|
|
179
|
+
|
|
180
|
+
[variants.shelf_gridfinity_3x2.accepted]
|
|
181
|
+
sliver = 26
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`build`, `check`, `card` and `export` all walk a part's variants the same way they walk
|
|
185
|
+
its default, so a variant gets its own STL, its own baselines and its own line in the
|
|
186
|
+
card's generated block. Four of the sixteen parts in `examples/notch` are variants; the
|
|
187
|
+
alternative was four near-copies of two files, free to drift.
|
|
188
|
+
|
|
189
|
+
## For an agent
|
|
190
|
+
|
|
191
|
+
The doctrine lives in the package and prints with `nurb rules`: printability, load paths,
|
|
192
|
+
the polish pass, the kernel traps, and what to verify. `SKILL.md` and `AGENTS.md` are ten
|
|
193
|
+
lines each pointing at it, so there is one copy and it cannot drift.
|
|
194
|
+
|
|
195
|
+
A part explains itself in a card next to it, same basename. Most of it is written by
|
|
196
|
+
hand, including a `## Don't` section that records what was tried and rejected, which is
|
|
197
|
+
the only place that information exists. One fenced block is generated:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
nurb card
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
That block holds what only a build can tell you: bounding box, volume, solid count,
|
|
204
|
+
sliver count against the accepted baseline, projection ratio, check verdict. It carries
|
|
205
|
+
no timestamp, so regenerating it on unchanged geometry produces no diff and a stale card
|
|
206
|
+
shows up in `git diff`. It deliberately does not repeat the parameters, because the
|
|
207
|
+
signature is the parameters and copying them would be the drift the contract forbids.
|
|
208
|
+
|
|
209
|
+
Dimensions an agent cannot derive go in `measurements.toml` with how they were obtained:
|
|
210
|
+
|
|
211
|
+
```toml
|
|
212
|
+
[bracket_pitch]
|
|
213
|
+
value = 25.16
|
|
214
|
+
unit = "mm"
|
|
215
|
+
how = "on-center spacing across a run of brackets, measured on the wall"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from nurb import measured
|
|
220
|
+
pitch = measured("bracket_pitch")
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Asking for something that isn't there raises and says so. That failure is the point: a
|
|
224
|
+
guessed dimension produces a part that builds, checks clean, and prints.
|
|
225
|
+
|
|
226
|
+
`nurb render <part>` writes `build/<part>.png` by screenshotting the viewer, so the image
|
|
227
|
+
is what a human would see. It needs the optional extra, which is the only part of nurb
|
|
228
|
+
that wants a browser:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
uv sync --extra render && uv run playwright install chromium
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Tests
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
uv run pytest
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The parts in `examples/` are part of the suite, asserted against the dimensions and
|
|
241
|
+
baselines their catalog cards recorded in Fusion. `tests/test_notch_fit.py` is the
|
|
242
|
+
hanging interface: every channel floor on exact pitch, at full span, one per bracket and
|
|
243
|
+
no more, for every shipped configuration. Its numbers are literals rather than imports
|
|
244
|
+
from the part's own constants, because a fit test that reads the same constant the part
|
|
245
|
+
built from agrees with the part however wrong the constant is.
|
|
246
|
+
|
|
247
|
+
## The viewer is the configurator
|
|
248
|
+
|
|
249
|
+
A part's parameters were always introspectable, so the sliders come from the signature
|
|
250
|
+
and nothing else. The `stl` and `step` buttons build the part at whatever the sliders
|
|
251
|
+
are holding, at full polish whatever the preview economy, and hand back the file: what
|
|
252
|
+
is on screen is what lands in the slicer. Point somebody at your `nurb dev` and they
|
|
253
|
+
can configure and download a part without touching Python.
|
|
254
|
+
|
|
255
|
+
## Not built yet
|
|
256
|
+
|
|
257
|
+
- A hosted configurator. `nurb dev` already is one for anybody who can reach it, but
|
|
258
|
+
publishing without a running kernel is a different problem: MakerWorld's customizer
|
|
259
|
+
runs OpenSCAD, which build123d does not transpile to.
|
|
260
|
+
- Measurement tools in the viewer. The section view shows an interior; it does not
|
|
261
|
+
yet measure it.
|
|
262
|
+
- `min_wall` probes sample faces, so a pinch nothing lands near is still missed. A
|
|
263
|
+
clean result means "no thin walls found", not "no thin walls".
|
|
264
|
+
|
|
265
|
+
## Debugging the viewer
|
|
266
|
+
|
|
267
|
+
`window.__nurb` exposes `{ THREE, scene, camera, controls, mesh, ready }`.
|
|
268
|
+
|
|
269
|
+
The URL takes `?part=<name>` to open a part, `?view=iso|front|back|left|right|top` to
|
|
270
|
+
frame it deterministically, and `?bare` to hide the chrome. `nurb render` drives exactly
|
|
271
|
+
that, and waits on `ready`.
|
|
272
|
+
|
|
273
|
+
three.js is vendored in `src/nurb/vendor/three`, so the viewer needs no network. See the
|
|
274
|
+
README beside it before changing versions: the import graph has grown since r169 and the
|
|
275
|
+
files it added fail as a blank canvas rather than as an error.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
[FSL-1.1-MIT](LICENSE). Source-available for any purpose except building a competing
|
|
280
|
+
product, and converts to plain MIT two years after each release.
|
|
281
|
+
|
|
282
|
+
Copyright 2026 Ordinary Systems LLC.
|
|
283
|
+
|
|
284
|
+
### Third-party notices
|
|
285
|
+
|
|
286
|
+
nurb uses **Open CASCADE Technology** (OCCT) for all B-rep geometry, reached through
|
|
287
|
+
[build123d](https://github.com/gumyr/build123d) (Apache-2.0) and the `OCP` bindings
|
|
288
|
+
(Apache-2.0). OCCT is licensed under
|
|
289
|
+
[LGPL-2.1 with an additional exception](https://dev.opencascade.org/resources/licensing).
|
|
290
|
+
|
|
291
|
+
nurb does not redistribute OCCT. It is installed separately as a dependency, and
|
|
292
|
+
dynamically linked at runtime. If you ever bundle nurb into a single-file
|
|
293
|
+
distribution that embeds the OCCT binaries, ship a copy of the OCCT license with it
|
|
294
|
+
and keep the library replaceable, per LGPL.
|
|
295
|
+
|
|
296
|
+
nurb **does** redistribute [three.js](https://threejs.org) r169 (MIT), vendored in
|
|
297
|
+
`src/nurb/vendor/three` so the viewer works without a network. Its `LICENSE` ships
|
|
298
|
+
beside it and the `@license` header stays on the build file, which is what MIT asks for.
|
|
299
|
+
|
|
300
|
+
Other dependencies: trimesh (MIT), watchdog (Apache-2.0), websockets (BSD-3-Clause),
|
|
301
|
+
numpy (BSD-3-Clause). Optional, for `nurb render` only: playwright (Apache-2.0), which
|
|
302
|
+
downloads its own browser build.
|
nurb-0.1.0/README.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# nurb
|
|
2
|
+
|
|
3
|
+
Agentic CAD for 3D printing.
|
|
4
|
+
|
|
5
|
+
A part is a Python function. Its keyword defaults are its parameters. `nurb dev`
|
|
6
|
+
watches your parts, rebuilds them on save, and pushes new geometry to a browser
|
|
7
|
+
without moving your camera.
|
|
8
|
+
|
|
9
|
+
Built on [build123d](https://build123d.readthedocs.io) (OCCT), so parts are real
|
|
10
|
+
B-rep solids with working chamfers, fillets, and STEP export.
|
|
11
|
+
|
|
12
|
+
## Try it
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv run nurb new dispenser
|
|
16
|
+
uv run nurb dev # http://127.0.0.1:7373, or the next free port
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Edit `parts/dispenser.py` and watch it update.
|
|
20
|
+
|
|
21
|
+
## A part
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from nurb import *
|
|
25
|
+
|
|
26
|
+
@part
|
|
27
|
+
def dispenser(width=80.0, height=120.0, wall=2.0, draft=False):
|
|
28
|
+
body = Box(width, height, wall)
|
|
29
|
+
if draft:
|
|
30
|
+
return body
|
|
31
|
+
bed = body.bounding_box().min.Z
|
|
32
|
+
keep = body.edges().filter_by(lambda e: e.bounding_box().min.Z > bed)
|
|
33
|
+
return polish(body, keep, 1.0)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`draft` is optional and passed by the runtime, not the caller. When it's true the
|
|
37
|
+
part should skip its polish pass. `nurb dev` builds in draft by default: on this
|
|
38
|
+
trivial part it's 18ms polished vs 1ms draft, and on a real one the saving is
|
|
39
|
+
nearer 20%.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
nurb new <name> create parts/<name>.py and its card
|
|
45
|
+
nurb dev watch, rebuild, serve the viewer
|
|
46
|
+
nurb build [part] build once and report size
|
|
47
|
+
nurb check [part] run the printability rules
|
|
48
|
+
nurb rules print the design doctrine
|
|
49
|
+
nurb card [part] regenerate a card's AUTO block
|
|
50
|
+
nurb verify [part] run the doctrine's verification list
|
|
51
|
+
nurb render [part] write a PNG into build/
|
|
52
|
+
nurb export [part] write STL and STEP into build/, --formats for GLB
|
|
53
|
+
nurb extract find duplication across parts
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A project is any directory with a `parts/` folder. There's no init step, and there's
|
|
57
|
+
no such thing as being outside a project: `mkdir -p thing/parts && cd thing && nurb new
|
|
58
|
+
clip` is the whole setup for a one-off.
|
|
59
|
+
|
|
60
|
+
`nurb dev` serves one project, so two projects means two of them. It takes 7373 if that
|
|
61
|
+
is free and walks up if it is not, printing where it landed, and the sidebar and the
|
|
62
|
+
browser tab both carry the project name so two of them are not mistakable for each
|
|
63
|
+
other.
|
|
64
|
+
|
|
65
|
+
Names are deliberately boring. The primary user is a language model, and a model that
|
|
66
|
+
has never seen this tool can guess `build`, `check` and `export`. It cannot guess a
|
|
67
|
+
themed alias.
|
|
68
|
+
|
|
69
|
+
## Why a long-lived process
|
|
70
|
+
|
|
71
|
+
Importing build123d costs 45s cold and 2.3s warm, and that is the whole argument: the
|
|
72
|
+
dev server pays it once instead of on every save.
|
|
73
|
+
|
|
74
|
+
What a rebuild costs after that depends on the part. A simple one is 29ms to build and
|
|
75
|
+
1ms to tessellate. The heaviest part in `examples/` is 401ms and 30ms. Draft mode is
|
|
76
|
+
not the lever it looks like: chamfers are 23% of that build, not most of it.
|
|
77
|
+
|
|
78
|
+
Tessellation used to be the larger half, at 620ms on that part, and almost none of it
|
|
79
|
+
was geometry. `Shape.tessellate` reads its triangles with `for t in poly.Triangles()`,
|
|
80
|
+
and OCP's iterator over that array costs 536ms where reading the same 7790 triangles by
|
|
81
|
+
index costs 6.8ms. `builder._triangulate` does the latter and returns bit-identical
|
|
82
|
+
vertices and faces. It is worth knowing before optimising the wrong thing.
|
|
83
|
+
|
|
84
|
+
## Layout
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
parts/<name>.py the part
|
|
88
|
+
parts/<name>.md its card: what it is, why, what not to retry
|
|
89
|
+
system.py optional: shared constants and geometry, importable from a part
|
|
90
|
+
measurements.toml optional: real-world dimensions with how they were obtained
|
|
91
|
+
printer.toml optional: which machine this project prints on
|
|
92
|
+
build/ generated, gitignored
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Cards are colocated with parts and share a basename. That's the whole link; a
|
|
96
|
+
rename is `git mv` on two files.
|
|
97
|
+
|
|
98
|
+
## Checks
|
|
99
|
+
|
|
100
|
+
`nurb check` runs the printability rules against the solid rather than an exported
|
|
101
|
+
mesh, so it sees real faces with exact areas and normals instead of triangles.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
overhang downward faces past 45 degrees, bridges told from cantilevers
|
|
105
|
+
min_wall thinnest section, ray cast corrected by an inscribed sphere
|
|
106
|
+
sliver faces too small to print as anything but a smear
|
|
107
|
+
concave_cosmetic polish laid into an inside corner
|
|
108
|
+
bed_bevel polish laid on the edges that meet the build plate
|
|
109
|
+
stability center of mass outside the footprint
|
|
110
|
+
projection_ratio reach over height, for a part cantilevered off a wall
|
|
111
|
+
build_volume does it fit the printer at all
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`min_wall`'s ray is exact on flat parallel walls and measures the slant through a skewed
|
|
115
|
+
one, so any chord thin enough to change the verdict is corrected by the largest sphere
|
|
116
|
+
tangent at that point, computed against the solid with exact kernel distances. A sphere
|
|
117
|
+
whose far contact is a graze rather than a wall is rejected by the same 0.3 cosine floor
|
|
118
|
+
the ray's exit filter uses, which is what keeps a detent dimple's bowl from reading as a
|
|
119
|
+
thin section of the web it is pressed into.
|
|
120
|
+
|
|
121
|
+
The bed size belongs to the machine, not to a part, so it is not written on cards.
|
|
122
|
+
A project picks a shipped profile once, in `printer.toml` at the root:
|
|
123
|
+
|
|
124
|
+
```toml
|
|
125
|
+
profile = "bambu_a1_mini"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Any check setting can be overridden in the same file, machine-wide. A card still wins
|
|
129
|
+
for what its part has justified. `nurb check --printer prusa_mk4s` answers "does this
|
|
130
|
+
fit that machine" without touching the file, and naming a profile that does not exist
|
|
131
|
+
lists the ones that do.
|
|
132
|
+
|
|
133
|
+
Every part carries what it has already justified on its card, so a known finding is
|
|
134
|
+
silent and a new one is a regression:
|
|
135
|
+
|
|
136
|
+
```toml
|
|
137
|
+
[part]
|
|
138
|
+
min_wall = 1.0
|
|
139
|
+
|
|
140
|
+
[accepted]
|
|
141
|
+
sliver = 6
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
It reports by default and takes `--strict` for CI, on the grounds that a warning which
|
|
145
|
+
blocks work gets switched off. Findings also show up in `nurb dev`, with a pin on the
|
|
146
|
+
geometry at each one.
|
|
147
|
+
|
|
148
|
+
## Variants
|
|
149
|
+
|
|
150
|
+
Some parts in a catalog are the same function flexed rather than new geometry. Those
|
|
151
|
+
ship as variants on the card, not as copies of the file:
|
|
152
|
+
|
|
153
|
+
```toml
|
|
154
|
+
[variants.shelf_gridfinity_3x2.params]
|
|
155
|
+
grid_x = 3
|
|
156
|
+
bracket_count = 6
|
|
157
|
+
|
|
158
|
+
[variants.shelf_gridfinity_3x2.accepted]
|
|
159
|
+
sliver = 26
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`build`, `check`, `card` and `export` all walk a part's variants the same way they walk
|
|
163
|
+
its default, so a variant gets its own STL, its own baselines and its own line in the
|
|
164
|
+
card's generated block. Four of the sixteen parts in `examples/notch` are variants; the
|
|
165
|
+
alternative was four near-copies of two files, free to drift.
|
|
166
|
+
|
|
167
|
+
## For an agent
|
|
168
|
+
|
|
169
|
+
The doctrine lives in the package and prints with `nurb rules`: printability, load paths,
|
|
170
|
+
the polish pass, the kernel traps, and what to verify. `SKILL.md` and `AGENTS.md` are ten
|
|
171
|
+
lines each pointing at it, so there is one copy and it cannot drift.
|
|
172
|
+
|
|
173
|
+
A part explains itself in a card next to it, same basename. Most of it is written by
|
|
174
|
+
hand, including a `## Don't` section that records what was tried and rejected, which is
|
|
175
|
+
the only place that information exists. One fenced block is generated:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
nurb card
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
That block holds what only a build can tell you: bounding box, volume, solid count,
|
|
182
|
+
sliver count against the accepted baseline, projection ratio, check verdict. It carries
|
|
183
|
+
no timestamp, so regenerating it on unchanged geometry produces no diff and a stale card
|
|
184
|
+
shows up in `git diff`. It deliberately does not repeat the parameters, because the
|
|
185
|
+
signature is the parameters and copying them would be the drift the contract forbids.
|
|
186
|
+
|
|
187
|
+
Dimensions an agent cannot derive go in `measurements.toml` with how they were obtained:
|
|
188
|
+
|
|
189
|
+
```toml
|
|
190
|
+
[bracket_pitch]
|
|
191
|
+
value = 25.16
|
|
192
|
+
unit = "mm"
|
|
193
|
+
how = "on-center spacing across a run of brackets, measured on the wall"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from nurb import measured
|
|
198
|
+
pitch = measured("bracket_pitch")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Asking for something that isn't there raises and says so. That failure is the point: a
|
|
202
|
+
guessed dimension produces a part that builds, checks clean, and prints.
|
|
203
|
+
|
|
204
|
+
`nurb render <part>` writes `build/<part>.png` by screenshotting the viewer, so the image
|
|
205
|
+
is what a human would see. It needs the optional extra, which is the only part of nurb
|
|
206
|
+
that wants a browser:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
uv sync --extra render && uv run playwright install chromium
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Tests
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
uv run pytest
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The parts in `examples/` are part of the suite, asserted against the dimensions and
|
|
219
|
+
baselines their catalog cards recorded in Fusion. `tests/test_notch_fit.py` is the
|
|
220
|
+
hanging interface: every channel floor on exact pitch, at full span, one per bracket and
|
|
221
|
+
no more, for every shipped configuration. Its numbers are literals rather than imports
|
|
222
|
+
from the part's own constants, because a fit test that reads the same constant the part
|
|
223
|
+
built from agrees with the part however wrong the constant is.
|
|
224
|
+
|
|
225
|
+
## The viewer is the configurator
|
|
226
|
+
|
|
227
|
+
A part's parameters were always introspectable, so the sliders come from the signature
|
|
228
|
+
and nothing else. The `stl` and `step` buttons build the part at whatever the sliders
|
|
229
|
+
are holding, at full polish whatever the preview economy, and hand back the file: what
|
|
230
|
+
is on screen is what lands in the slicer. Point somebody at your `nurb dev` and they
|
|
231
|
+
can configure and download a part without touching Python.
|
|
232
|
+
|
|
233
|
+
## Not built yet
|
|
234
|
+
|
|
235
|
+
- A hosted configurator. `nurb dev` already is one for anybody who can reach it, but
|
|
236
|
+
publishing without a running kernel is a different problem: MakerWorld's customizer
|
|
237
|
+
runs OpenSCAD, which build123d does not transpile to.
|
|
238
|
+
- Measurement tools in the viewer. The section view shows an interior; it does not
|
|
239
|
+
yet measure it.
|
|
240
|
+
- `min_wall` probes sample faces, so a pinch nothing lands near is still missed. A
|
|
241
|
+
clean result means "no thin walls found", not "no thin walls".
|
|
242
|
+
|
|
243
|
+
## Debugging the viewer
|
|
244
|
+
|
|
245
|
+
`window.__nurb` exposes `{ THREE, scene, camera, controls, mesh, ready }`.
|
|
246
|
+
|
|
247
|
+
The URL takes `?part=<name>` to open a part, `?view=iso|front|back|left|right|top` to
|
|
248
|
+
frame it deterministically, and `?bare` to hide the chrome. `nurb render` drives exactly
|
|
249
|
+
that, and waits on `ready`.
|
|
250
|
+
|
|
251
|
+
three.js is vendored in `src/nurb/vendor/three`, so the viewer needs no network. See the
|
|
252
|
+
README beside it before changing versions: the import graph has grown since r169 and the
|
|
253
|
+
files it added fail as a blank canvas rather than as an error.
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
[FSL-1.1-MIT](LICENSE). Source-available for any purpose except building a competing
|
|
258
|
+
product, and converts to plain MIT two years after each release.
|
|
259
|
+
|
|
260
|
+
Copyright 2026 Ordinary Systems LLC.
|
|
261
|
+
|
|
262
|
+
### Third-party notices
|
|
263
|
+
|
|
264
|
+
nurb uses **Open CASCADE Technology** (OCCT) for all B-rep geometry, reached through
|
|
265
|
+
[build123d](https://github.com/gumyr/build123d) (Apache-2.0) and the `OCP` bindings
|
|
266
|
+
(Apache-2.0). OCCT is licensed under
|
|
267
|
+
[LGPL-2.1 with an additional exception](https://dev.opencascade.org/resources/licensing).
|
|
268
|
+
|
|
269
|
+
nurb does not redistribute OCCT. It is installed separately as a dependency, and
|
|
270
|
+
dynamically linked at runtime. If you ever bundle nurb into a single-file
|
|
271
|
+
distribution that embeds the OCCT binaries, ship a copy of the OCCT license with it
|
|
272
|
+
and keep the library replaceable, per LGPL.
|
|
273
|
+
|
|
274
|
+
nurb **does** redistribute [three.js](https://threejs.org) r169 (MIT), vendored in
|
|
275
|
+
`src/nurb/vendor/three` so the viewer works without a network. Its `LICENSE` ships
|
|
276
|
+
beside it and the `@license` header stays on the build file, which is what MIT asks for.
|
|
277
|
+
|
|
278
|
+
Other dependencies: trimesh (MIT), watchdog (Apache-2.0), websockets (BSD-3-Clause),
|
|
279
|
+
numpy (BSD-3-Clause). Optional, for `nurb render` only: playwright (Apache-2.0), which
|
|
280
|
+
downloads its own browser build.
|