Last active
February 26, 2026 20:11
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
View raw
(Sorry about that, but we can’t show files that are this big right now.)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment