3D Solids
Box(width, depth, height,
x=0, y=0, z=0, hole=False, fill=None,
align="center", center=False, fillet=0,
opacity=100)
Sized along x/y/z in that order. The base sits at z -- it is not centered on z like it is on x and y, unless center=True.
Cylinder(radius, height,
x=0, y=0, z=0, hole=False, fill=None,
align="center", center=False,
segments=32, fillet=0, opacity=100)
Stands upright along z. Base sits at z, same as Box.
Polyhedron(points, faces,
fill=None, opacity=100, hole=False)
The rawest shape: matches OpenSCAD's polyhedron. points is a list of (x, y, z) corners; faces is a list of point NUMBERS (not coordinates) naming, in order, which corners trace each flat face -- e.g. a triangle from points 0, 1, 2 is face (0, 1, 2). Order matters within a face: list its corners walking around the OUTSIDE in one consistent direction (the right-hand-rule convention) so the face's normal points outward. Get it backwards and the shape still renders (every material here is double-sided) but that face's shading looks inside-out. Faces are fan-triangulated from their own first point, so this is only guaranteed correct for convex (or triangular) faces -- a genuinely non-convex face can triangulate wrong. See the Polyhedrons lesson for a full walkthrough of points vs. faces.
align="left-top" / "top" / "right-top" /
"left" / "center" / "right" /
"left-bottom" / "bottom" / "right-bottom"
Like CMU's align: a 9-way grid that changes what (x, y) points at. Redefined here since z is vertical, not y: "top" means farther away from you (+y), "bottom" means closer to you (-y).
center=True
Like OpenSCAD's center: also centers the shape on z, instead of sitting with its base at z.
segments=6
Like OpenSCAD's $fn (a leading $ isn't legal in Python). How many flat faces approximate a curve -- low numbers turn Cylinder into a visible prism (6 = hexagon, 3 = triangle). Cylinder only.
fillet=0.5
Rounds edges/corners with a smooth curve (a "fillet", not a "chamfer" -- a chamfer is a flat cut). On Box: all edges and corners. On Cylinder: just the rim, where the flat cap meets the curved side. Same units as everything else. Clamped automatically so it can't exceed the shape itself.
SEGMENTS = 6
A global default for segments=, like OpenSCAD's $fn special variable (as a plain variable instead, since a leading $ isn't legal in Python). Set it once, anything drawn after that point picks it up unless it gives its own segments=. Resets to 32 every Run, so it never carries over from a previous run.
2D Profiles
These render right away, as a thin flat slab (fill=/opacity=/hole= all work, same as any 3D shape) -- extrude()/rotate_extrude() replace that default thinness with real height, same as Rect/Circle in CMU graphics.
Rect(left, top, width, height,
fill=..., opacity=100, hole=False, fillet=0)
fillet rounds the corners in the 2D profile -- extruded straight up, that rounds only the vertical edges. The flat top/bottom stay sharp (that's Box's own fillet= instead).
Circle(centerX, centerY, radius,
segments=32, fill=..., opacity=100, hole=False)
Oval(centerX, centerY, width, height,
fill=..., border=None, borderWidth=2,
opacity=100, rotateAngle=0, dashes=False,
align="center", hole=False)
CMU parity shape. border draws a real outlined stroke (a fat Line2, so borderWidth actually changes thickness, unlike a plain WebGL line) -- dropped if the Oval gets folded into a union()/etc., same as any other preview-only property. left/top/right/bottom read and write like CMU's, and centerX/centerY/width/height/rotateAngle stay live -- change any of them after creation and the shape updates, which none of the other 2D profiles here do yet.
Star(centerX, centerY, radius, points,
fill=..., border=None, borderWidth=2,
roundness=None, opacity=100, rotateAngle=0,
dashes=False, align="center", hole=False)
CMU parity shape, same border=/left-right-top-bottom/live-mutation deal as Oval, including a settable width/height (a star only has one real size knob -- radius -- so setting either moves the same radius, keeping the bounding box square). points is the point count (radius>0, points>2, both required); star.points reads that count back, not an outline. roundness (0-100) fillets every vertex, inner and outer alike, using the same corner-rounding math as Rect/RegularPolygon's fillet= -- at 100 that erases the points into a soft blob. CMU doesn't document its own inner-point ratio or exact roundness math, so both are a reasonable approximation here, not a pixel-for-pixel match.
RegularPolygon(centerX, centerY,
radius, numSides, rotateAngle=0,
fillet=0, fill=..., opacity=100, hole=False)
Polygon(x1, y1, x2, y2, x3, y3, ...,
fill=..., opacity=100, hole=False)
At least 3 points.
Label(text, x, y, size=1,
font="helvetiker", bold=False, italic=False,
fill=..., opacity=100, hole=False)
Centered at (x, y), like CMU's Label. font: helvetiker (default, no extra load) / arial / monospace / cursive / caveat / montserrat.
Boolean Operations
union(*solids, hole=False, fill=...,
opacity=...)
Merges non-hole shapes into one solid. If some of what you pass in are already hole=True, those get cut into the non-hole ones immediately instead of just being glued on -- an all-hole group stays a hole, just bigger. Only matters if you're mixing hole=True shapes in; plain shapes behave exactly like a normal merge.
difference(base, *subtract,
hole=False, fill=..., opacity=...)
base with every other shape cut out of it -- always cuts, regardless of whether base/subtract happen to be marked hole=True themselves.
intersection(*solids,
hole=False, fill=..., opacity=...)
Only the overlap of every shape survives -- NOT "first minus the rest" (that's difference).
Leave fill=/opacity= out and they're inherited from the first (or base) shape -- not literally None/100, which would mean something different (fill=None specifically means hole=True). Give the boolean call its own fill=/opacity= to override that inheritance.
a.add(b) /
a.subtract(b)
Grow or cut a shape in place -- a stays the same object, just bigger or with a bite taken out, instead of union()/difference() making a brand new one. add() is hole-aware the same way union() is: adding a hole=True shape cuts it into a instead of gluing it on. subtract() always cuts, like difference().
a + b /
a - b
Same as union(a, b) / difference(a, b) -- makes a new shape, both operands consumed.
a += b /
a -= b
Same as a.add(b) / a.subtract(b) -- mutates a in place. Python gives += its own meaning separate from +, which lines up with add() being "in place" and + being "make something new."
Modifier Characters
OpenSCAD marks these with a symbol (*, !, #, %) right before a shape. Python has no room for a prefix character before a function call, so these are attributes instead -- set on any shape, same as x= or fill=. All four work no matter how deep a shape is buried inside a union()/difference().
shape.visible = False
OpenSCAD's * -- completely excluded, as if commented out. Not rendered, not part of the real geometry, not in the STL. Every shape starts out visible=True; this is also exactly what happens automatically the moment a shape gets consumed by translate()/rotate()/union()/etc., so a shape that's stopped rendering on its own and one you've deliberately hidden are the same thing under the hood, not two separate ideas.
shape.show_only = True
OpenSCAD's ! -- only this shape renders; everything else in the whole design is set aside until you remove it.
shape.debug = True
OpenSCAD's # -- draws an extra highlighted (magenta, translucent) copy of this shape on top of the normal result. Doesn't change what the shape actually contributes to a union/difference -- it's purely a visual callout.
shape.background = True
OpenSCAD's % -- the opposite of debug: shown as a faint gray ghost for reference, but excluded from the real geometry and the STL entirely, same as visible=False.
Opacity (preview only)
Like CMU's opacity: 0-100, not 0-1. 100 (the default) is fully solid, 0 is invisible. Also never reaches the exported STL. Works everywhere -- every 3D shape, extrude()/rotate_extrude(), and 2D profiles like Rect/Circle themselves, since those render on their own now too.
Box(2, 2, 2, fill="cornflowerblue", opacity=40)
opacity=100
opacity=70
opacity=40
opacity=15
Interactive
All optional. Defined or not, Run/Reset/Stop/Export work exactly the same either way.
def onKeyPress(key): ...
Called on every key press, like CMU's onKeyPress. Click the viewer first to give it keyboard focus (shown with a blue outline and a hint). Named keys match CMU: "Up" / "Down" / "Left" / "Right" / "Space". Letters and digits come through as themselves ("a", "A", "1").
def onNext(): ...
Not from CMU -- our own addition. Defining it adds a Next button next to Stop; each click calls it again. Handy for a manual, step-by-step walkthrough instead of a key-driven one.
shape = Box(2, 2, 2)
shape.x += 3
Every shape stays a live, mutable handle -- any field you passed in (x, fill, opacity, radius, whatever the shape takes) can be read or reassigned later. This is what makes onKeyPress/onNext useful: keep a reference from your first run, then move or restyle it from inside the handler instead of redrawing everything from scratch. Calling Box()/Cylinder()/etc. again inside a handler still works too -- it just adds a new shape each time rather than replacing one.
print(...)
Works everywhere -- top-level code, onKeyPress, onNext. Output appears in a small panel between the code and the toolbar, but only when there's something to show: no output, no panel. A print() right before a crash still shows, so it's there for the case you need it most.
A real Run resets everything -- shape registry, any variables/functions you defined, all of it -- back to a clean slate. State only survives between individual onKeyPress/onNext calls within the same run.