ScadPy¶
Programmatic CAD in Pure Python.
ScadPy provides a fluent, type-safe API for 2D and 3D parametric modeling, built on Shapely and trimesh. Write designs with the conciseness of OpenSCAD and the full power of Python.
Installation¶
pip install scadpy
Requirements: Python ≥ 3.12.
Quick examples¶
>>> # 2D — chamfered mounting plate
>>> from scadpy import circle, cuboid, rectangle, sphere, square, text
>>> from scadpy import x, y, z, GRAY, ORANGE
>>> import numpy as np
>>> PLATE_WIDTH = 80
>>> PLATE_HEIGHT = 50
>>> PLATE_THICKNESS = 10
>>> HOLE_RADIUS = 4
>>> HOLE_MARGIN = 10
>>> CHAMFER_SIZE = 8
>>> base = rectangle([PLATE_WIDTH, PLATE_HEIGHT])
>>> plate = base.chamfer(CHAMFER_SIZE)
>>> for position, normal in zip(base.vertex_coordinates, base.vertex_normals):
... hole_center = position - HOLE_MARGIN * np.sqrt(2) * normal
... plate -= circle(HOLE_RADIUS).translate(hole_center)
>>> plate.to_screen()
>>> # 3D — extrude mounting plate with label
>>> TEXT_THICKNESS = 2
>>> extruded_plate = plate.linear_extrude(PLATE_THICKNESS)
>>> label = text("ScadPy", size=15).linear_extrude(TEXT_THICKNESS)
>>> extruded_plate |= label.translate(z(PLATE_THICKNESS))
>>> extruded_plate.to_screen()
>>> # 3D — parametric ball bearing
>>> BALL_RADIUS = 3
>>> RACE_RADIUS = 15
>>> NB_BALLS = 11
>>> CLEARANCE = 0.1
>>> RING_HEIGHT = 7
>>> RACE_THICKNESS = 10
>>> groove = circle(BALL_RADIUS + CLEARANCE) | rectangle([BALL_RADIUS, RING_HEIGHT])
>>> race = rectangle([RACE_THICKNESS, RING_HEIGHT]) - groove
>>> bearing = race.radial_extrude(axis=y(), pivot=x(RACE_RADIUS)).color(GRAY)
>>> ball = sphere(BALL_RADIUS).color(ORANGE)
>>> for angle in np.linspace(0, 360, NB_BALLS, endpoint=False):
... bearing += ball.rotate(angle, axis=y(), pivot=x(RACE_RADIUS))
>>> bearing.to_screen()
>>> # 3D — dice
>>> SIZE = 20
>>> dice = cuboid(SIZE)
>>> pip = sphere(SIZE / 12).translate(z(SIZE / 2))
>>> one = pip
>>> two = (
... pip.translate([SIZE / 4, SIZE / 4, 0]) +
... pip.translate([-SIZE / 4, -SIZE / 4, 0])
... )
>>> three = one + two
>>> four = two + two.rotate(90, z())
>>> five = one + four
>>> six = four + pip.translate(x(SIZE / 4)) + pip.translate(x(-SIZE / 4))
>>> dice -= (
... one
... + two.rotate(90, x())
... + three.rotate(90, y())
... + four.rotate(-90, y())
... + five.rotate(-90, x())
... + six.rotate(-180, x())
... )
>>> dice.to_screen()
>>> # 3D — storage box
>>> SIZE_OUTER = 20
>>> SIZE_INNER = 18
>>> FILLET = 1
>>> BASE_HEIGHT = 10
>>> CUT_HEIGHT = 8
>>> CAP_HEIGHT_OUTER = 1.5
>>> CAP_HEIGHT_INNER = 3
>>> CAP_OFFSET_X = 25
>>> CUT_OFFSET_Z = 2
>>> outer_base = square(SIZE_OUTER).fillet(FILLET).linear_extrude(BASE_HEIGHT)
>>> inner_cut = square(SIZE_INNER).linear_extrude(CUT_HEIGHT).translate(z(CUT_OFFSET_Z))
>>> base = outer_base - inner_cut
>>> cap_outer = square(SIZE_OUTER).fillet(FILLET).linear_extrude(CAP_HEIGHT_OUTER)
>>> cap_inner = square(SIZE_INNER).linear_extrude(CAP_HEIGHT_INNER)
>>> cap = (cap_outer | cap_inner).translate(x(CAP_OFFSET_X))
>>> storage_box = base + cap
>>> storage_box.to_screen()
Cheat sheet¶
Parameters shown in # comments are optional, with their default values.
2D — Shape
from scadpy import *
# primitives
circle(radius=3) # segment_count=64
polygon(points=[(-2, -2), (2, -2), (0, 2)])
rectangle(size=[6, 3])
Shape.from_dxf("file.dxf")
Shape.from_svg("file.svg")
square(size=4)
# boolean operations
s = square(size=4); c = circle(radius=3)
s | c # union
s - c # difference
s & c # intersection
s ^ c # symmetric difference
s + c # concat (no merge)
# transforms
s.chamfer(size=0.8) # vertex_filter=None, epsilon=1e-8
s.color(color=RED)
s.convexify() # part_filter=None
s.fill() # part_filter=None
s.fillet(size=0.8) # vertex_filter=None, segment_count=32, epsilon=1e-8
s.grow(distance=0.5) # part_filter=None
s.linear_cut(axis=x()) # pivot=0
s.linear_slice(thickness=2, direction=x()) # pivot=0, part_filter=None
s.mirror(normal=[1, 0]) # pivot=0
s.pull(distance=1.0) # pivot=0, vertex_filter=None
s.push(distance=1.0) # pivot=0, vertex_filter=None
s.radial_slice(start=0, end=180) # pivot=0, part_filter=None
s.resize(size=[6, None]) # auto=False, pivot=None, vertex_filter=None
s.rotate(angle=30) # pivot=0, vertex_filter=None
s.scale(scale=[2, 0.5]) # pivot=0, vertex_filter=None
s.shrink(distance=0.5) # part_filter=None
s.translate(translation=[2, 1]) # vertex_filter=None
# topology — coordinates & attributes
s.are_vertices_convex # (n_vertices,) — convexity mask
s.directed_edge_directions # (2*n_edges, 2)
s.edge_lengths # (n_edges,)
s.edge_midpoints # (n_edges, 2)
s.edge_normals # (n_edges, 2)
s.ring_types # (n_rings,) — "exterior"|"interior"
s.vertex_angles # (n_vertices,) — interior angles (°)
s.vertex_coordinates # (n_vertices, 2)
s.vertex_normals # (n_vertices, 2) — outward unit normals
# topology — bridges (*_to_*)
s.directed_edge_to_edge # directed_edge → edge
s.directed_edge_to_vertex # directed_edge → [start, end]
s.edge_to_vertex # edge → [start, end]
s.ring_to_part # ring → part
s.vertex_to_incoming_directed_edge # vertex → directed_edge
s.vertex_to_outgoing_directed_edge # vertex → directed_edge
s.vertex_to_neighbor_vertex # vertex → [prev, next]
s.vertex_to_part # vertex → part
s.vertex_to_ring # vertex → ring
# extrusions → Solid
s.linear_extrude(height=3)
s.radial_extrude(axis=y(), pivot=x(5)) # start=0, end=360, segment_count=64
# export
s.to_dxf_file("output.dxf")
s.to_html_file("output.html")
s.to_screen()
s.to_svg_file("output.svg")
3D — Solid
from scadpy import *
# primitives
cone(radius=2, height=4) # section_count=32
cuboid(size=[4, 3, 2])
cylinder(radius=2, height=4) # section_count=32
polyhedron(vertices=vertices, faces=faces)
sphere(radius=3) # subdivision_count=4
Solid.from_stl("model.stl")
# boolean operations
a = cuboid(size=[4, 3, 2]); b = sphere(radius=2)
a | b # union
a - b # difference
a & b # intersection
a ^ b # symmetric difference
a + b # concat (no merge)
# transforms
a.color(color=RED)
a.convexify() # part_filter=None
a.mirror(normal=[1, 0, 0]) # pivot=0
a.pull(distance=1.0) # pivot=0, vertex_filter=None
a.push(distance=1.0) # pivot=0, vertex_filter=None
a.resize(size=[6, None, None]) # auto=False, pivot=None, vertex_filter=None
a.rotate(angle=30, axis=z()) # pivot=0, vertex_filter=None
a.scale(scale=[2, 1, 0.5]) # pivot=0, vertex_filter=None
a.translate(translation=[1, 0, 0]) # vertex_filter=None
# topology — coordinates & bridges (*_to_*)
a.triangle_to_vertex # triangle → [v0, v1, v2]
a.vertex_coordinates # (n_vertices, 3)
a.vertex_to_part # vertex → part
# export
a.to_html_file("output.html")
a.to_screen()
a.to_stl_file("output.stl")
Roadmap¶
Improve documentation
Richer topology for Shape and Solid
Richer transformations for Shape and Solid
Chamfer and fillet on Solid
New assembly types:
PointCloud2d,Wire2d,PointCloud3d,Wire3dBetter error messages
More import/export formats
Development¶
# Create and activate venv
python3 -m venv .venv
source .venv/bin/activate
# Install with dev dependencies
pip install -e .[dev]
# Run doctests & generate documentation
cd docs && make doctest && make html
License¶
See LICENSE.md at the root of the repository.
API reference¶
- core
- assembly
- combinations
- topologies
get_assembly_directed_edge_directions()get_assembly_directed_edge_to_edge()get_assembly_directed_edge_to_vertex()get_assembly_edge_lengths()get_assembly_edge_midpoints()get_assembly_edge_normals()get_assembly_face_vertex_angles()get_assembly_face_vertex_normals()get_assembly_face_vertex_to_incoming_directed_edge()get_assembly_face_vertex_to_outgoing_directed_edge()get_assembly_part_colors()get_assembly_vertex_coordinates()get_assembly_vertex_to_part()
- transformations
- types
- utils
- component
- part
- assembly
- 2D
- shape
- combinations
- exporters
- features
- importers
- primitives
- topologies
are_shape_vertices_convex()get_shape_directed_edge_directions()get_shape_directed_edge_to_edge()get_shape_directed_edge_to_vertex()get_shape_edge_lengths()get_shape_edge_midpoints()get_shape_edge_normals()get_shape_edge_to_vertex()get_shape_part_vertex_coordinates()get_shape_ring_to_part()get_shape_ring_types()get_shape_vertex_angles()get_shape_vertex_coordinates()get_shape_vertex_normals()get_shape_vertex_to_incoming_directed_edge()get_shape_vertex_to_neighbor_vertex()get_shape_vertex_to_outgoing_directed_edge()get_shape_vertex_to_part()get_shape_vertex_to_ring()
- transformations
chamfer_shape()color_shape()convexify_shape()fill_shape()fillet_shape()grow_shape()linear_cut_shape()linear_extrude_shape()linear_slice_shape()mirror_shape()pull_shape()push_shape()radial_extrude_shape()radial_slice_shape()recoordinate_shape()resize_shape()rotate_shape()scale_shape()shrink_shape()translate_shape()
- types
- shape
- 3D