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 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.