Skip to content

Instantly share code, notes, and snippets.

@gwpl
Last active February 26, 2026 20:11
Show Gist options
  • Select an option

  • Save gwpl/7e0302cc5e70e5675f2792416199207c to your computer and use it in GitHub Desktop.

Select an option

Save gwpl/7e0302cc5e70e5675f2792416199207c to your computer and use it in GitHub Desktop.
CadQuery Lego Brick Models — Parametric 3D generators with STL, SVG & PNG export. Includes: 6x2 thin brick, vase-on-2x2-brick. Run with: uv run --with=cadquery <script>.py or use make
#!/usr/bin/env -S uv run --with=cadquery
# Script can be run with ./lego_brick_cadquery.py after chmod +x lego_brick_cadquery.py
# thanks to uv shebang that runs script `--with=cadquery`
# otherwise run with `uv run --with=cadquery lego_brick_cadquery.py`
#
# <https://cadquery.readthedocs.io/en/latest/examples.html#lego-brick>
#
# Lego brick generator. Based on the dimensions of a standard Lego brick,
# this script generates a 3D model of a Lego brick with specified dimensions
# (number of bumps long and wide, and whether it's thin or thick).
# The script uses the CadQuery library to create the 3D model and exports:
# - STL file for 3D printing
# - SVG vector renderings (upper and lower isometric views)
# - PNG raster renderings (converted from SVG, if rsvg-convert is available)
import cadquery as cq
import os
import subprocess
import shutil
import sys
print("Started...")
#####
# Inputs
######
lbumps = 6 # number of bumps long
wbumps = 2 # number of bumps wide
thin = True # True for thin, False for thick
#
# Lego Brick Constants-- these make a Lego brick a Lego :)
#
pitch = 8.0
clearance = 0.1
bumpDiam = 4.8
bumpHeight = 1.8
if thin:
height = 3.2
else:
height = 9.6
t = (pitch - (2 * clearance) - bumpDiam) / 2.0
postDiam = pitch - t # works out to 6.5
total_length = lbumps * pitch - 2.0 * clearance
total_width = wbumps * pitch - 2.0 * clearance
# make the base
s = cq.Workplane("XY").box(total_length, total_width, height)
# shell inwards not outwards
s = s.faces("<Z").shell(-1.0 * t)
# make the bumps on the top
s = (
s.faces(">Z")
.workplane()
.rarray(pitch, pitch, lbumps, wbumps, True)
.circle(bumpDiam / 2.0)
.extrude(bumpHeight)
)
# add posts on the bottom. posts are different diameter depending on geometry
# solid studs for 1 bump, tubes for multiple, none for 1x1
tmp = s.faces("<Z").workplane(invert=True)
if lbumps > 1 and wbumps > 1:
tmp = (
tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True)
.circle(postDiam / 2.0)
.circle(bumpDiam / 2.0)
.extrude(height - t)
)
elif lbumps > 1:
tmp = (
tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True)
.circle(t)
.extrude(height - t)
)
elif wbumps > 1:
tmp = (
tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True)
.circle(t)
.extrude(height - t)
)
else:
tmp = s
result = tmp
# Output directory: same directory as the script
script_dir = os.path.dirname(os.path.abspath(__file__))
base_name = "lego_brick_cadquery"
def svg_to_png(svg_path, png_path, width=1024):
"""Convert SVG to PNG using rsvg-convert, inkscape, or imagemagick (whichever is available)."""
for cmd_name, cmd in [
("rsvg-convert", ["rsvg-convert", "-w", str(width), "-o", png_path, svg_path]),
("inkscape", ["inkscape", svg_path, f"--export-filename={png_path}", f"-w{width}"]),
("convert", ["convert", "-background", "white", "-density", "150", svg_path, png_path]),
]:
if shutil.which(cmd_name):
try:
subprocess.run(cmd, check=True, capture_output=True)
return True
except subprocess.CalledProcessError:
continue
return False
# SVG export options for different isometric views
# projectionDir is the direction vector FROM which the camera looks AT the object
svg_views = {
"upper": {
"projectionDir": (-1.75, 1.1, 5), # default: looking from above-front-left
"label": "upper isometric (top + front view)",
},
"lower": {
"projectionDir": (-1.75, 1.1, -5), # looking from below-front-left
"label": "lower isometric (bottom + front view)",
},
}
svg_common_opts = {
"width": 800,
"height": 400,
"marginLeft": 100,
"marginTop": 20,
"showAxes": False,
"strokeWidth": 0.5,
"strokeColor": (0, 0, 0),
"hiddenColor": (160, 160, 160),
"showHidden": True,
}
# --- SVG Rendering ---
for view_name, view_cfg in svg_views.items():
svg_path = os.path.join(script_dir, f"{base_name}_{view_name}.svg")
print(f"Generating SVG ({view_cfg['label']}): {svg_path} ...")
opts = {**svg_common_opts, "projectionDir": view_cfg["projectionDir"]}
cq.exporters.export(result, svg_path, exportType="SVG", opt=opts)
print(f" Created {svg_path}")
# Convert SVG to PNG
png_path = os.path.join(script_dir, f"{base_name}_{view_name}.png")
print(f"Generating PNG from SVG: {png_path} ...")
if svg_to_png(svg_path, png_path):
print(f" Created {png_path}")
else:
print(f" WARNING: No SVG-to-PNG converter found (tried rsvg-convert, inkscape, imagemagick).", file=sys.stderr)
# --- STL Export ---
stl_path = os.path.join(script_dir, f"{base_name}.stl")
print(f"Generating STL: {stl_path} ...")
result.val().exportStl(stl_path, ascii=True)
print(f" Created {stl_path}")
print("Done!")
# For CQ-editor:
# show_object(result)
# CadQuery Lego Brick Models — Build all exports
#
# Prerequisites:
# - uv (https://docs.astral.sh/uv/)
# - For PNG conversion (optional): rsvg-convert, inkscape, or imagemagick
#
# Usage:
# make # build all models (STL + SVG + PNG)
# make brick # build only the 6x2 lego brick
# make vase # build only the vase-on-brick model
# make clean # remove all generated files
UV = uv run --with=cadquery
# Output files
BRICK_OUTPUTS = lego_brick_cadquery.stl \
lego_brick_cadquery_upper.svg lego_brick_cadquery_upper.png \
lego_brick_cadquery_lower.svg lego_brick_cadquery_lower.png
VASE_OUTPUTS = vase_on_lego_brick.stl \
vase_on_lego_brick_upper.svg vase_on_lego_brick_upper.png \
vase_on_lego_brick_lower.svg vase_on_lego_brick_lower.png
.PHONY: all brick vase clean
all: brick vase
brick: $(BRICK_OUTPUTS)
vase: $(VASE_OUTPUTS)
$(BRICK_OUTPUTS): lego_brick_cadquery.py
$(UV) python $<
$(VASE_OUTPUTS): vase_on_lego_brick.py
$(UV) python $<
clean:
rm -f *.stl *.svg *.png
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment