SCADwright is a Python library for designing 3D parts and assemblies: you write Python; SCADwright generates an OpenSCAD source file that renders into STL (or any other format OpenSCAD supports).
While OpenSCAD offers a straight-forward and easy path to programmatic 3d design, it is severely limited in ways that rapidly get annoying once your project grows beyond a few parts.
SCADwright keeps the basic OpenSCAD model and primitives, but SCADwright goes way beyond just a python wrapper, offering additional functionality not easily possible with OpenSCAD:
- Components written without boilerplate that can:
- accept any set of arguments from which the rest can be calculated, using defined equations (rather than multiple definitions or complex conditional logic)
- enforce constraints and validate arguments, also through the same equations
- expose attributes and calculations to other parts of the code (as well as their bounding boxes and attachment points)
- a library of reusable shapes out of the box
- auto-EPS adjustment on difference() and union()
- surface-based attachment, in a single chaining command
- placing text on geometric objects (plates, cylinders, cones) is easy
- ability to add new transforms to the language
- smart centering built into new components by default
- separate print/display/integration/debug arrangements of your part, with one-line animations between them
- python-style chaining (e.g.
shape.linear_extrude(height=15).left(10), rather than OpenSCAD's transform-first and object-last notation) - scripts you can parametrize from the command line (including via json)
- real error messages with line numbers
- automated tests
- parts know their own bounding box without rendering — print-bed checks, overlap tests
$fnis managed automatically, but easy to override- manufacturing fudges (extra clearances, material variance) are tunable, without interfering with finished-part design dimensions (or parts that reference them)
and more.
While simple projects very strongly resemble OpenSCAD code (easy to be productive immediately), as your projects grow in complexity, SCADwright allows a graceful transition to more complex features, without any hard syntactic or conceptual boundaries. Styles can be mixed and matched in the same project.
I have put significant effort into refining the UX of SCADwright: the more advanced constructs use a syntax that's neither OpenSCAD nor quite standard object-oriented python. Instead, the goal is to ruthlessly eliminate boiler plate, and make constructs simple, elegant, and highly expressive.
SCADwright optimizes for common cases, for those with little background in object-oriented python or advanced OpenSCAD, but retains full python capabilities and a low-level interface for exceptional cases.
SCADwright calls OpenSCAD only at render time. The Python side requires sympy (pulled in automatically by pip) and nothing else. I've taken some care to make emitted SCAD human-readable.
If you're comparing SCADwright against SolidPython, PythonSCAD, CadQuery, Build123d, or other Python+CAD tools, see How is SCADwright different? for a side-by-side.
The quick start / organizing a project guide is the best place to see the power of SCADwright in action, but there's also full documentation and examples projects.
Less than 60 lines of SCADwright gives you: parabolic ogive nose, bulged body, three parabolic-swept fins with rounded edges, a flared nozzle, a tapered helicoid stem with an almond cross-section, a filleted M2-counterbored baseplate, and correctly spaced text (both raised and inset) on a doubly curved surface.
When you write a parametric module in OpenSCAD — say a bracket with mount-hole positions — the caller has no way to ask where those holes are. You are forced to compute the offsets outside the object, or in two places, or hard-code them.
In SCADwright, parametric parts are Python classes called Components. The caller can read any attribute of other Components freely, and use that information to construct other parts to fit.
from scadwright import Component
from scadwright.primitives import cube
class Bracket(Component):
equations = "width, height > 0"
def build(self):
return cube([self.width, self.width, self.height])
b = Bracket(width=80, height=5)
print(b.width) # readable; no geometry built yetFor scenarios where multiple parts rely on the same mechanical interface, or when you want to separate direct measurements from your code, SCADWright offers Specs.
2. Define a component's equations once, call it with any combination of sufficient arguments, the framework handles the rest; no boilerplate. Components are as nice to write as they are to call.
Consider a hollow tube has an outer diameter, an inner diameter, and a wall thickness, linked by od = id + 2*thk.
In OpenSCAD you either write three modules (tube_by_id_thk, tube_by_od_thk, tube_by_id_od) or one module with conditional logic. The relationship lives in a comment; the code just enumerates cases. And if a wall thickness must be positive, you write an assert() that fires at render time — after you've already waited.
In SCADwright, you write a Component, declaring relationships and constraints together as equations.
When you want to use the Component, supply whatever combination of arguments you want. The framework automatically works out a solution, if it can. Otherwise, you get a specific error: malformed (not an equation), insufficient (provided arguments can't solve the equations), inconsistent (constraint failed, or provided arguments generate inconsistent solutions), and ambiguous (multiple discrete solutions, usually fixable with a >0 constraint).
You don't even need to isolate a variable on the left of the equation like you do with programming languages.
from scadwright import Component
class Tube(Component):
equations = """
od - id = 2*thk # structural relationship: solve for the missing one
h, id, od, thk > 0 # constraints
"""
def build(self): ...
Tube(h=10, id=8, thk=1) # od solved = 10
Tube(h=10, id=8, od=10) # thk solved = 1
Tube(h=10, od=10, thk=1) # id solved = 8
Tube(h=10, id=8, thk=-1) # ValidationError: thk must be positiveTrigonometry uses degrees, like OpenSCAD.
class RightTriangle(Component):
equations = """
tan(alpha) = opp/adj
cos(alpha) = adj/hyp
"""
def build(self): ...
RightTriangle(opp=3, adj=4) # solves for alpha = 36.87, hyp = 5
RightTriangle(hyp=5, alpha=36.87) # solves for opp = 3, adj = 4Equations aren't limited to scalar arithmetic. Either side can be any Python expression: build a tuple with a comprehension, pick between values with a conditional, read fields and items off the inputs.
class BatteryHolder(Component):
spec = Param(BatterySpec)
equations = """
count:int > 0 # constraint
wall_thk, clearance, end_clearance > 0 # constraint
pitch = spec.d + 2*clearance # field read on a namedtuple input
outer_w = count * pitch + 2*end_clearance # arithmetic
positions = tuple(i*pitch for i in range(count)) # tuple from a comprehension
len(positions:tuple) = count # redundant consistency
"""Equations also handle optional inputs. Prefix a name with ? to mark it optional; arithmetic that bottoms out on an unset value silently skips, while the exactly_one / at_least_one / at_most_one / all_or_none helpers and ? inside conditionals enforce or branch on presence explicitly.
class FilletedBracket(Component):
equations = """
w, l, thk > 0
exactly_one(?r, ?r_frac) # absolute radius or fraction-of-thk; one and only one
edge = ?r if ?r else ?r_frac * thk # synthesize the active value
edge < thk / 2 # downstream check uses the synthesized name
"""
def build(self): ...
FilletedBracket(w=40, l=20, thk=4, r=1) # absolute
FilletedBracket(w=40, l=20, thk=4, r_frac=0.25) # 25% of thk
FilletedBracket(w=40, l=20, thk=4) # ValidationError: exactly_one(?r, ?r_frac) — neither set
FilletedBracket(w=40, l=20, thk=4, r=1, r_frac=0.25) # ValidationError: both setIn OpenSCAD, when two shapes share a face in a difference() or union(), the result has artifacts unless you manually extend the shapes by a tiny epsilon. Every project defines eps = 0.01 and litters it through every cut and join.
SCADwright automatically eliminates the need for epsilon overlap.
from scadwright.boolops import difference, union
from scadwright.primitives import cube, cylinder
box = cube([20, 20, 10])
part = difference(box, cylinder(h=10, r=3).through(box)) # through-hole, no manual epsthrough(parent) detects which faces of the cutter are flush with the parent and extends them automatically. For joints, attach(fuse=True) overlaps parts at the contact face.
OpenSCAD has no built-in module library, only primitives.
SCADwright offers a shape library with 50+ ready-made Components across geometric, mechanical, fastener, gear, and print-oriented categories. These eliminate sub-tasks, so you don't have to roll your own tubes, rounded rectangles, screw holes, or look up M3 bolt head diameters, hex profiles, and clearance hole sizes each time. Did I mention gears?
from scadwright.shapes import Tube, SpurGear, Bolt, HexNut, HoneycombPanel, Bearing
cap = Tube(h=10, id=8, thk=1) # od solved: 10
gear = SpurGear(module=2, teeth=20, h=5) # involute profile; .pitch_r readable on the instance
bolt = Bolt(size="M3", length=10) # ISO dimensions from data tables
bearing = Bearing.of("608") # 8x22x7, ready for fit-check
panel = HoneycombPanel(size=(80, 60, 3), cell_size=8, wall_thk=1)Every shape is a Component — you can read its computed dimensions, attach other parts to it, and pass it into boolean operations. See the shape library docs for the full catalog.
Fit tolerances flow project-wide. Set Clearances(sliding=0.05, press=0.08, snap=0.2, finger=0.2) once on your Design class and every AlignmentPin, PressFitPeg, SnapPin, and TabSlot inherits automatically; override per-scope, per-Component, or per-call. See Clearances.
In OpenSCAD you can't write cube(10).chamfer_top(depth=1) — there's no way to add a transform that works on any shape.
In SCADwright, register a transform once and it becomes a method on every shape:
from scadwright.boolops import minkowski
from scadwright.primitives import cube, sphere
from scadwright.transforms import transform
@transform("chamfer_top")
def chamfer_top(node, *, depth):
return minkowski(node, sphere(r=depth, fn=8))
part = cube([10, 10, 5]).chamfer_top(depth=1)In OpenSCAD, putting a label on a part means doing the math yourself: build the 2D text(), linear_extrude it, then translate/rotate it onto the face — and that only works on a flat face. Wrapping a label around a cylinder or up the side of a funnel means hand-rolling per-glyph placement around an arc, or giving up.
SCADwright provides add_text() as a chained method on every shape. One call places raised or inset text on any flat face, cylindrical wall, conical wall, or disk rim:
from scadwright.primitives import cube, cylinder
from scadwright.shapes import Tube, Funnel
plate = cube([60, 30, 2], center="xy")
plate.add_text(label="HELLO", relief=0.5, on="top", font_size=8) # raised on a flat face
plate.add_text(label="v1.0", relief=-0.3, on="top", font_size=4, offset=(0, -8)) # inset, offset within the face
cyl = cylinder(h=20, r=10)
cyl.add_text(label="BRAND", relief=0.4, on="outer_wall", font_size=4,
angle="front") # wrapped around the cylinder
Funnel(h=30, bot_od=20, top_od=40, thk=2).add_text(
label="0.5L", relief=0.4, on="outer_wall", font_size=4, text_orient="slant", # wraps a tapered cone
)
Tube(h=30, od=24, thk=2).add_text(
label="LOT 7", relief=-0.3, on="inner_wall", font_size=4, # text on the inside surface
)
cylinder(h=10, r=15).add_text(label="MAX 5L", relief=0.4, on="top", font_size=3) # arc-wrapped along the rimrelief is signed: positive raises, negative cuts (and cuts deeper than the wall punch through).
Multi-line labels stack the right way for each surface — vertically on a face, axially on a wall, radially on a rim — and the host's anchors survive the call, so labels chain and attach() still works afterwards.
See add_text() for the full reference.
wrap_2d() does the same for a 2D profile (an imported SVG logo, a polygon, an outline), placing it as raised or inset relief on a flat face or a curved wall:
from scadwright.primitives import cylinder, scad_import
logo = scad_import("logo.svg", bbox=((0, 0, 0), (124, 106, 0))) # bbox = the imported mm extent
cylinder(h=80, r=25).wrap_2d(profile=logo, relief=-0.8, on="outer_wall", size=60) # wrapped, insetprojection="wrap" keeps proportions on a cylinder; projection="flat" presses the profile straight onto a sphere, cone, or other curved wall at uniform depth. See wrap_2d() for the full reference.
Often the best way to print a part is very different from how you want to see it. A part might need supports, or to be re-oriented, or cut in half for printing. For display, you might want to see parts mated together or show stand-in hardware.
In OpenSCAD this becomes commented-out blocks, duplicated files, or fragile flags.
SCADwright has a Design class with named @variant methods:
from scadwright.boolops import union
from scadwright.design import Design, run, variant
class Widget(Design):
box = MyBox()
lid = MyLid(box=box)
@variant(fn=128, default=True)
def print(self):
return union(self.box, self.lid.right(80))
@variant(fn=48)
def display(self):
return union(self.box, self.lid.up(self.box.height))
if __name__ == "__main__":
run()scadwright build widget.py --variant=print
scadwright build widget.py --variant=display
Animating between variants is a one-liner using morph (as long as same parts appear in the variants).
In OpenSCAD, stacking a lid on a box means computing translate([0, 0, box_height]) by hand. If you add a spacer or change a dimension, every downstream offset needs updating.
SCADwright's attach() method lets you position parts by naming which faces should touch:
from scadwright.primitives import cube, cylinder
plate = cube([40, 40, 2])
peg = cylinder(h=10, r=3).attach(plate) # bottom on top
cap = cube([8, 8, 2]).attach(peg, on="top") # cap on top of pegInsert a spacer between any two parts and nothing downstream needs to change. Components can declare custom named anchors for semantically meaningful attachment points.
See Attaching shapes for the full attach() reference, and Anchors for declaring custom attachment points on Components.
In OpenSCAD, the only way to know how big something is (whether it fits on your print bed, whether two parts overlap, whether a lid is wider than its box) is to render it and eyeball the result.
SCADwright computes bounding boxes from the AST, without rendering. You can query them, assert against them, and use them to position parts relative to each other:
from scadwright import bbox
from scadwright.asserts import assert_fits_in, assert_no_collision
bb = bbox(my_widget)
print(bb.size) # (width, length, height)
# Or as a chained property on any shape — no import needed:
print(my_widget.bbox.size)
assert_fits_in(my_widget, [200, 200, 50]) # fits on the print bed?
assert_no_collision(box, lid) # parts don't overlap?Note: bboxes have limitations - it's the smallest cube that fits your part. But it's a hell of a lot better than nothing, and going further verges into rewriting OpenSCAD.
In OpenSCAD, center=true works on primitives but not on modules. If your module builds a shape at the origin and you want it centered, you compute the offset yourself. Every module that needs centering reinvents the same translate-by-half-size logic.
In SCADwright, every Component accepts center= as a constructor kwarg — same syntax as cube(center=...), with per-axis control:
from scadwright.shapes import UShapeChannel
u = UShapeChannel(wall_thk=2, channel_length=50, channel_width=10, center="xy")
u.outer_width # still readable — it's still a ComponentThe Component author doesn't write any centering code. The framework computes the bounding box after build() and translates the requested axes to the origin. For Components where the geometric center isn't the right reference point, override center_origin() to return a custom one.
In OpenSCAD, the verb comes before the noun: you write the rotate-then-translate first, then the shape they apply to. Reading the code, you have to scan to the end of a line to see what's actually moving.
SCADwright puts the shape first. Operations chain off the shape:
from scadwright.primitives import cube
cube([10, 20, 30]).up(5).rotate([0, 45, 0]).red()OpenSCAD's error messages typically point at the rendered output, not your source.
Tracking down which call produced a bad value is manual.
SCADwright errors carry the file and line of your call:
from scadwright.primitives import cube
cube([-5, 10, 10])
# ValidationError: cube size[0] must be non-negative, got -5.0 (at widget.py:42)Component equation errors name the equation that failed and the values that didn't line up — so an over-specified or inconsistent call doesn't make you re-read the equations block to find which line broke:
Tube(h=10, id=8, thk=1, od=11) # over-specified AND inconsistent: 8 + 2*1 ≠ 11
# ValidationError: Tube.equations[0]: equation violated: `od - id = 2*thk` (lhs=3.0, rhs=2.0)OpenSCAD takes -D foo=10, but scripts can't say what parameters they accept, what types they expect, or what defaults to use. The contract lives in comments.
SCADwright scripts declare parameters explicitly:
from scadwright import arg, render
from scadwright.boolops import difference
from scadwright.primitives import cube, cylinder
width = arg("width", default=40, type=float, help="widget width in mm")
MODEL = difference(
cube([width, width, 20], center="xy"),
cylinder(h=22, r=5, center=True),
)
render(MODEL, "widget.scad")scadwright build widget.py --width=80
scadwright build widget.py --help # lists arguments with defaults
For inputs that don't fit nicely on the command line (a list of holes with positions and diameters, or a parts table), put them in a JSON file and read it inside the script with from_json(). Run with scadwright build widget.py --from-json holes.json.
In OpenSCAD, you either set $fn globally (too coarse) or pass it to every single primitive call (tedious and easy to miss one). There's no middle ground.
In SCADwright, resolution (fn, fa, fs) flows automatically through the hierarchy. Set it once at the level that makes sense and every primitive below inherits it:
from scadwright.shapes import Tube
# Per-instance: pass fn when constructing a Component
cap = Tube(h=10, id=8, thk=1, fn=64)
# Per-variant: set fn in the @variant decorator and every Component
# and primitive built inside that variant inherits it
@variant(fn=48, default=True)
def print(self):
return self.housing # all primitives inside get fn=48
# Per-scope: wrap any block of code
with resolution(fn=128):
high_res_part = difference(sphere(r=10), sphere(r=8))No declaration needed on the Component side — fn is accepted by every Component automatically and flows into the resolution context for its build() method.
15. Manufacturing-specific fudges are easy to add, track, and keep separate from design considerations
The need to occasionally lie about a part's dimensions to manufacture what you really want is a sad fact of reality.
In OpenSCAD, you might write d = 14.5 + 0.3 — but now other parts read your lie literally, and a year later you have no idea what that meant.
SCADwright provides Specs: small frozen classes that hold shared dimensions and run the same equations block Components use.
Inside that block, adjustments layer corrections with +=, -=, *=, /= on their own lines with comments, separate from the design value:
from scadwright import Spec
class CamMount(Spec):
equations = """
cam_barrel_od = 60
surround = 10
cam_barrel_od + surround <= 70 # rule sees cam_barrel_od = 60, not the post-adjust 60.35
cam_barrel_od += 0.3 # printer X-axis overshoot
cam_barrel_od += 0.05 # extra slop for the o-ring
"""
CamMount.cam_barrel_od # 60.35The fudge-lies needed to make cam_barrel_od come out right don't interfere with design constraints.
The chain is queryable to see what adjustments are applied and where they come from.
CamMount.adjustments_for("cam_barrel_od")
# [Adjustment(line=4, delta=0.3, comment='printer X-axis overshoot'),
# Adjustment(line=5, delta=0.05, comment='extra slop for the o-ring')]Specs can take optional ? inputs (printer profile, material), and passing different ones in each variant lets one project produce calibrated builds for several printers from a single source.
OpenSCAD has no way to write a regression test that says "this part hasn't changed since I last reviewed it." You either re-render and visually compare, or trust that your edit didn't break anything.
SCADwright hashes the geometry tree so you can pin a part's shape in a unit test:
from scadwright import tree_hash
def test_widget_geometry_pinned():
assert tree_hash(Widget(width=40)) == "a1b2c3d4e5f6..."If any dimension, transform, or boolean op changes, the hash changes and the test fails — before you ever open OpenSCAD.
from scadwright import render
from scadwright.boolops import difference
from scadwright.primitives import cube, cylinder
body = cube([40, 40, 20], center="xy")
hole = cylinder(h=22, r=5, center=True, fn=64)
part = difference(
body,
hole.right(10),
hole.left(10),
)
render(part, "widget.scad")Run with python widget.py (writes widget.scad) or use the CLI: scadwright build widget.py. Open the result in OpenSCAD to render.
When a part has named dimensions and relationships between them, wrap it in a Component:
from scadwright import Component, render
from scadwright.boolops import difference
from scadwright.primitives import cylinder
class Tube(Component):
equations = """
od = id + 2*thk
h, id, od, thk > 0
"""
def build(self):
return difference(
cylinder(h=self.h, r=self.od / 2),
cylinder(h=self.h + 2, r=self.id / 2).down(1),
)
t = Tube(h=30, id=20, thk=2) # od solved = 24.0
print(t.od) # 24.0 — readable without rendering
render(t, "tube.scad")Building on the Tube above, add a Design with variants — a display view showing the tube upright, and a print view that halves it into two concave-down pieces spaced apart for the print bed:
from scadwright import Component, bbox
from scadwright.boolops import difference, union
from scadwright.design import Design, run, variant
from scadwright.primitives import cylinder
class Tube(Component):
equations = """
od = id + 2*thk
h, id, od, thk > 0
"""
def build(self):
return difference(
cylinder(h=self.h, r=self.od / 2),
cylinder(h=self.h + 2, r=self.id / 2).down(1),
)
class MyTube(Tube):
h = 30
id = 20
thk = 2
class TubeProject(Design):
tube = MyTube()
@variant(fn=64, default=True)
def display(self):
return self.tube
@variant(fn=64)
def print(self):
half = self.tube.halve([0, -1, 0]) # cut in half along Y
spacing = bbox(half).size[1] + 5
return union(
half, # concave side down
half.forward(spacing),
)
if __name__ == "__main__":
run()scadwright build tube.py # display variant (default)
scadwright build tube.py --variant=print # two halves, bed-ready
Download the latest release from the releases page and unzip it, then from inside the unzipped directory:
pip install '.[lsp,curved-text]'
The scadwright command becomes available. [lsp] adds editor integration; [curved-text] adds proportional glyph spacing on curved surfaces. Both are recommended, but optional.
SCADwright requires sympy.
Three optional extras layer in deeper functionality:
pip install 'scadwright[lsp]'— addspyglsfor the language server. Editors with an LSP client get inline diagnostics, completion, hover, goto-definition, and project-wide rename insideequationsblocks. See Editor integration below.pip install 'scadwright[curved-text]'— addsfreetype-pyfor proportional glyph spacing inadd_texton cylindrical, conical, and rim surfaces. Without it, those paths fall back to a uniform-width heuristic.pip install -e '.[dev]'— addspytestfor running the test suite from a clone. Combine with[lsp]if you also want to work on the language server:pip install -e '.[dev,lsp]'.
If you try hard enough to eliminate boiler plate, sooner or later you wind up with a DSL (design-specific language) (or you reinvent a worse form of LISP). While it's tempting to embrace that in an ecstatic frenzy of ascetic pursuit, the hangover is a total lack of IDE support for your weird syntax. The world, it seems, does not appreciate beauty.
So I wrote us a Language Server Protocol (LSP).
Any editor with an LSP client gets full inline support for equations = """...""" blocks: red squiggles for every error the resolver would raise, completion for the curated math/builtins namespace and the surrounding class's declarations, hover with declared types and defaults, goto-definition, document symbols, and project-wide rename that updates cross-file references atomically.
Install with pip install 'scadwright[lsp]', then point your editor at scadwright lsp. Configs for Vim, Neovim, Helix, Emacs (eglot / lsp-mode), Sublime, Zed, VSCode, and PyCharm Pro live in docs/lsp_setup.md.
This project also includes VSCode and PyCharm extensions. Both add toolbar buttons (Preview / Render / Kill OpenSCAD) on Python files that import scadwright, plus syntax coloring inside equations blocks. Clicking Preview is enough to see the results of a change: as long as the generated filename is the same (same script, same variant), an already-open OpenSCAD window auto-reloads.
The VSCode extension bundles the LSP client. Install the [lsp] extra in your project venv and the extension spawns scadwright lsp automatically. The extension also provides the three Preview / Render / Kill toolbar actions and TextMate coloring inside equations blocks. See vscode/README.md for install steps and settings.
The PyCharm plugin provides the three toolbar actions plus PyCharm-native equations coloring and curated-namespace autocomplete (math functions, builtins, cardinality helpers, type tags, and constants, with auto-paren insertion on callables). PyCharm Community has no built-in LSP client, so the richer LSP features aren't available there. PyCharm Pro users can run scadwright lsp alongside the native plugin for diagnostics, Param-aware completion, hover, and project-wide rename — see docs/lsp_setup.md.
Compared to OpenSCAD, building a model with AI in SCADwright (and the MCP below) is faster and lets you take bigger steps than just building in OpenSCAD directly.
Claude Code summarized the key benefits of its working in SCADwright over OpenSCAD as:
- equations and constraints catch a lot of lazy or under-specified mistakes with explicit errors - ones that, in OpenSCAD, might otherwise might result in a less scrutable error or slip through entirely
- higher level abstractions (like
attach(fuse=True),through(), custom transforms,add_text(), etc.) are closer to what the user describes and require less interpretation/translation into code - AI is much better writing at python than openscad, reflecting the relative frequency of python in training data
Whichever AI assistant you use, dropping the style guide into its context steers generated code away from generic-Python habits toward SCADwright's idioms.
If you're developing with Claude Code, install the OpenSCAD MCP server. It gives Claude the ability to render your .scad output, visually inspect the result, and catch geometry errors without you having to open OpenSCAD yourself. SCADwright's generated SCAD is fully compatible — Claude can build your script, render it through the MCP, and iterate on the design in a tight feedback loop.
I've been working on the specification for this for years, long before vibe-coding was a thing.
Is there AI generated code in here? Yes. It's 2026 and I have a job. Sometimes a life too.
Is this one-shot-slop? No. It's the result of hundreds, maybe thousands, of incremental iterations, and a fair amount of hand-coding and human-writing (including this whole page here).
Pretty much every part of SCADwright has gone through at least 5-6 major revisions, reducing duplicate and boilerplate code, making the constructs intuitive and naively simple, and working through hard framework trade-offs, in detail through examples. I've been writing code longer than Python has been a language (and I've been writing serious code longer than Python has been a serious language) - I wouldn't put my name on something that's dogshit or poorly thought out. Hell, I even went to the effort to make the emitted SCAD human-readable.
In parallel, I'm actually using this framework for my current 3d printing projects: the Bronica S2 lens housing and convex lens caliper attachment both leverage this framework and motivated its completion.
I'm sure someone will accuse this project of pissing all over OpenSCAD's functional programming purity.
On the one hand, OpenSCAD isn't pure to begin with (Haskell syntax, but where's the type system and typeclasses?). On the other hand, SCADwright's equations syntax is pretty fucking FP (at least in the ways that FP isn't annoying).
At bottom, I'm far more interested in making SCADwright easy and widely usable than I am in expanding your consciousness through FP indoctrination.
I've taken great care to produce excellent documentation that's easy to consume. This is not an AI-generated afterthought, but rather carefully written docs for producing expressive and powerful code simply.
Full documentation here. Documentation is along the lines of the OpenSCAD Language Reference. There's also a cheatsheet that parallels the OpenSCAD cheatsheet.
For a quick intro, see How to organize a project.
This framework also includes examples of projects at various levels of difficulty.
If you're comparing SCADwright against SolidPython, PythonSCAD, CadQuery, Build123d, or other Python+CAD tools, see How is SCADwright different? for a side-by-side.

