Skip to main content

Parts

A Part is a solid. It may consist of several separate bodies (part.solids() lists them, a body inside a cavity of another too).

Primitives​

from smlibpy import *

Box(40, 30, 10) # length (X), width (Y), height (Z)
Cylinder(5, 20) # radius, height, along Z
Cone(10, 4, 15) # bottom radius, top radius, height
Cone(10, 0, 15) # a pointed cone
Sphere(8)
Torus(20, 4) # major and minor radius
Wedge(20, 10, 8) # a ramp
Pyramid(20, 12, 15) # a 20 x 12 base, the apex 15 above its center

All are centered on the origin; align changes that per axis:

Box(40, 30, 10, align=(Align.CENTER, Align.CENTER, Align.MIN)) # sits on the XY plane

Cylinders, cones, spheres and tori also come as sectors, from the +X axis counter-clockwise about Z, with exact faces (planes on the cut sides); AnnularSector is a sector of a tube. Like every primitive a sector is centered by its bounding box; align=None keeps the axis on Z:

Cylinder(10, 20, arc_size=90, align=None) # the quarter from +X to +Y
Cone(10, 4, 15, arc_size=120)
Sphere(10, arc_size1=0, align=None) # the upper half: latitudes 0 to 90
Sphere(10, arc_size1=-30, arc_size2=60, arc_size3=90)
Torus(20, 4, major_angle=90, align=None) # an elbow
AnnularSector(40, 50, 8, 30) # inner and outer radius, height, angle

A torus also keeps part of its tube: minor_start_angle and minor_end_angle (degrees, as build123d takes them) slice the tube's circle from its center, measured from the side away from the axis (0) towards +Z (90); the cut sides are planes, cylinders or cones:

Torus(20, 5, minor_start_angle=0, minor_end_angle=180) # the upper half, flat underneath
Torus(20, 5, minor_start_angle=-90, minor_end_angle=90) # the outer half: a bead on a cylinder

Extrude​

plate = Rectangle(40, 30).extrude(5) # along the sketch normal
both = Circle(5).extrude(10, both=True) # 10 to each side
drafted = Rectangle(30, 30).extrude(10, taper=8) # sides lean in by 8 degrees
flared = Rectangle(30, 30).extrude(10, taper=-8, corners="round") # out, corners rounded off
slanted = Circle(5).extrude(20, direction=(0, 1, 2))

The walls of a tapered extrusion are drafted planes, cones and cylinders (as Part.draft makes them: selections by type find them, they draft again, and STEP files carry them as such), B-spline surfaces where the profile has other curves. corners="round" rounds off the corners the taper pulls apart (the convex ones of an outward taper, the concave ones of an inward one), as an offset rounds them. A taper that closes the profile before the end raises ModelingError.

With a direction the amount is measured along it. A direction in the sketch plane raises ValueError, and one less than 0.06 degrees off the plane warns, since the solid is then thinner than a thousandth of the amount. The walls of straight edges are planes. An extrusion along a direction is checked: where the kernel makes it invalid (some curved profiles in tilted planes, mostly within a degree of the plane) it is made again turned about the normal, or swept along a line. A taper drafts the walls about the normal, so with a taper only a direction along the normal (either way) is taken; one across it raises ValueError, and so does a taper on an open curve.

A negative amount extrudes against the normal, which is how pockets are cut into faces:

part = Box(50, 50, 20)
part -= Rectangle(30, 20).on(part.faces().top()).extrude(-5)

Revolve​

Revolve a sketch about an axis in its plane. By default the axis is the sketch's local Y axis through its plane origin. A Polygon keeps its plane origin at the world origin, so on Plane.front the default axis is the world Z axis, the usual lathe setup. Centered constructors (Rectangle, Circle, ...) and make_face put the plane origin in the middle of the region: for Rectangle(10, 20).translate(15, 0).on(Plane.front) the default axis is the line x = 15, which crosses the region, so revolve() raises. Pass the axis:

section = Polygon((10, 0), (30, 0), (30, 5), (15, 5), (15, 20), (10, 20))
spool = section.on(Plane.front).revolve()
quarter = section.on(Plane.front).revolve(Z, 90)
tube = Rectangle(10, 20).translate(15, 0).on(Plane.front).revolve(Z)

The walls of a revolve are cylinders, cones, spheres and tori wherever its edges are lines and arcs, also for a full turn, so tube.faces().cylinders() finds both walls, Face.radius and drafts work on them, and STEP files carry them as such. Spline edges give surfaces of revolution. Angles within 0.01 degrees of 360 are a full turn.

Swing​

A swing revolves a profile about Z along an outline of any shape instead of a circle: at each height the section is the outline, scaled about the Z axis to pass through the profile on the +X side. The profile is drawn as for a lathe (x the distance from the axis, on Plane.front); the outline is a closed curve around the Z axis in XY that every ray from the axis crosses once:

cup = Polygon((0, 0), (30, 0), (30, 40), (0, 40)).on(Plane.front)
oval = swing(cup, Ellipse(30, 20)) # pi * 30 * 20 * 40
bottle = Polygon((0, 0), (20, 0), (20, 50), (8, 60), (8, 70), (0, 70)).on(Plane.front)
flask = bottle.swing(RoundedRectangle(40, 30, 8)) # 40 x 30, a 16 x 12 neck

The faces are exact products of the profile's and the outline's curves (B-splines). A circle about the origin gives the revolve; an open profile gives a surface.

Loft​

A loft blends a series of cross sections:

transition = loft([Rectangle(30, 30), Circle(12).translate(0, 0, 25)])
vase = loft([Circle(15), Circle(25).translate(0, 0, 30), Circle(10).translate(0, 0, 60)])
straight = loft([Rectangle(20, 20), RegularPolygon(10, 6).translate(0, 0, 20)], ruled=True)

start_tangent and end_tangent set the direction a loft leaves its first section in and reaches its last in, so that it meets the bodies there smoothly; closed=True runs it on from the last section through the first again (a ring without ends):

neck = loft([Circle(10), Circle(4).translate(0, 0, 30)], start_tangent=Z, end_tangent=Z)
bottle = Circle(10).extrude(-40) + neck + Circle(4).translate(0, 0, 30).extrude(10) # smooth joints
ring = loft([Rot(z=45 * i) * Pos(10, 0, 0) * Rot(x=90) * Circle(2 + i % 2) for i in range(8)], closed=True)

A tangent can also be a face that ends at the section: the loft continues it, tangent to it all along the section. rails are curves a loft of open sections runs along, through every section: the surface passes through them as through the sections, within about 2e-7:

can = Cylinder(10, 20)
rim = max((e for e in can.edges() if e.geom_type == "circle"), key=lambda e: e.center().z)
spout = loft([rim, Circle(4).translate(0, 0, 30)], start_tangent=can.faces().cylinders())

r1, r2 = Spline((0, 0, 0), (-2, 10, 1), (0, 20, 0)), Spline((20, 0, 0), (24, 10, 2), (20, 20, 0))
arcs = [ThreePointArc(r1.point_at(t), (r1.point_at(t) + r2.point_at(t)) * 0.5 + Vector(0, 0, 4), r2.point_at(t))
for t in (0, 0.5, 1)]
panel = loft(arcs, rails=[r1, r2]) # a Shell through the arcs and both rails

Sweep​

A sweep moves a profile along a path. smlibpy puts the center of the profile's box on the start of the path and turns it to face along the path, so profiles are simply drawn in XY. Along a path of several edges the profile keeps its orientation: tangent edges are swept in one go, and at a corner of a 3D path it turns by the smallest rotation from one tangent to the next. Where the kernel cannot sweep an open path that way, each edge is swept on its own, turned to its own start, with a warning unless the profile is round. A loop closed with a kink (Spline(*pts, pts[0])) is swept in two halves fused where its ends cross; where that fuse fails it is swept in pieces as before 0.5.0, and a round profile with its seam turned. Faces without area that such a fuse leaves are taken out, or the sweep raises ModelingError. A planar path of several edges gets mitered corners in any plane, with the profile placed as on any other path.

path = Spline((0, 0, 0), (0, 20, 30), (40, 20, 50))
tube = sweep(Circle(3) - Circle(2), path)

spring = sweep(Circle(1), Helix(pitch=5, height=30, radius=10))

frame = sweep(Rectangle(6, 4), Polyline((0, 0, 0), (0, 0, 40), (40, 0, 40))) # mitered corner
elbow = sweep(Circle(4), Wire([Line((0, 0, 0), (0, 0, 20)),
CenterArc((15, 20), 15, 180, -90, plane=Plane.front)]))

translational=True keeps the profile's orientation fixed along the path instead of turning it. anchor puts another point of the profile on the path: "origin" (the origin of the sketch's plane, (0, 0) of a Polygon drawn on XY) or a point of the profile's plane:

ell = Polygon((0, 0), (6, 0), (6, -1), (-1, -1), (-1, 5), (0, 5)) # an L drawn by its inner corner
trim = sweep(ell, path, anchor="origin") # the corner follows the path

An open profile that runs from the path to a second rail sweeps along both with rails: it is moved, turned and scaled so that its ends stay on them, and the surface passes through both (within about 2e-7):

left, right = Spline((0, 0, 0), (2, 15, 1), (0, 30, 0)), Spline((20, 0, 0), (26, 15, 3), (22, 30, 0))
hood = sweep(ThreePointArc((0, 0, 0), (10, 0, 4), (20, 0, 0)), left, rails=right)

A path that is a single line is swept as an extrusion, a single arc (or circle) as a revolve: planes, cylinders, cones and tori, not the B-spline surfaces of the kernel's sweep. A profile that reaches across the axis of the arc raises ModelingError (it would sweep through itself).

scale grows or shrinks the profile along the path, about the point of it that sits on the path: (start, end) evenly along the length, [(t, s), ...] through scales at fractions of the length (a smooth curve that does not overshoot them), or a function of the fraction:

horn = sweep(Circle(2), path, scale=(1, 3))
taper_spring = sweep(Circle(1), Helix(pitch=8, height=40, radius=15), scale=(1, 0.5))
bulge = sweep(Circle(2), Line((0, 0, 0), (0, 0, 40)), scale=[(0, 1), (0.5, 1.6), (1, 1)])

Along the path the profile turns as little as it can, which keeps its attitude to the plane of a planar path. Along a helix it keeps its attitude to the helix's axis instead (a thread tooth or a flat spring wire stays radial), also on conical helices and chains of helices. up keeps it at its angle to a direction (or, with up="frenet", to the path's curvature), and twist turns it about the path on top of that, by an angle spread along the path or a function of the fraction of its length:

flat_spring = sweep(Rectangle(2, 0.5), Helix(pitch=4, height=40, radius=10)) # stays radial
rail = sweep(Rectangle(4, 2), path, up=Z) # stays upright
bar = sweep(Rectangle(10, 2), Line((0, 0, 0), (0, 0, 100)), twist=180) # a half turn
ribbon = sweep(Line((-5, 0, 0), (5, 0, 0)), Line((0, 0, 0), (0, 0, 100)), twist=lambda s: 360 * s * s)

Round a closed path with corners that is not planar, the profile carried by the smallest turns comes back turned about the path; that turn is spread along it, so that the profile meets itself where the loop closes. Where up="frenet" would make the profile spin about a path that nearly straightens, the sweep raises ValueError: pick up a direction there.

A Helix takes any number of turns: the kernel fits up to 120 in one go, longer ones in pieces of whole turns joined into one edge (Helix(2, 400, 20), 200 turns, is 25135.87 long against 25135.92 for the exact helix). Sweeps along long helices go too (400 turns tested); with the kernel's own frame (orient=False) a long helix is swept in pieces.

Booleans and patterns​

block = Box(60, 40, 20)
block -= Cylinder(8, 30) # through hole
block += Cylinder(6, 10).translate(0, 0, 15) # boss on top
block &= Sphere(40) # keep what's inside a sphere

plate = Box(80, 60, 6) - GridLocations(60, 40, 2, 2) * Cylinder(3, 10)
wheel = Cylinder(40, 8) - PolarLocations(25, 6) * Cylinder(6, 10)

Holes​

Hole tools point down from the origin, so placed on a face they drill into it:

part = Box(60, 40, 10)
top = part.faces().top()
part -= Hole(3, 10).on(top) # plain, at the center
part -= (GridLocations(40, 20, 2, 2) * CounterBoreHole(2, 4, 3, 10)).on(top)
part -= (Pos(-20, 0) * CounterSinkHole(2, 4, 10)).on(top)

CounterBoreHole(radius, counter_bore_radius, counter_bore_depth, depth) and CounterSinkHole(radius, counter_sink_radius, depth, angle=82).

Moving and copying​

Parts move like every shape: translate, rotate(axis, angle), mirror(plane), scale(factor), moved(location) and on(plane).

half = Box(20, 10, 10).translate(10, 0, 0)
whole = half + half.mirror(Plane.YZ)

Next: features.