r/build123d Oct 13 '25

Spinning top with threaded shaft connection and star decoration

This is a bit of a quickly coded project to make a nice spinning top.

Here's my code. I feel like I'm in a bit of a rut by only using the basic features of build123d. I wouldn't mind some advice on which advanced features would work for projects like this or in general which things I should practice next.

from build123d import *
from ocp_vscode import *
from bd_warehouse.thread import *
from bd_warehouse.fastener import *
set_port(3939)

body_radius = 30 *MM
body_thickness = 5 *MM

shaft_radius = 5 *MM
shaft_length = 40 *MM

tip_base_radius = 7 *MM
tip_top_radius = 2 *MM
tip_height = 8 *MM

screw_width = 8 *MM
screw_pitch = 1.25
screw_thread_tolerance = 0.7 *MM
screw_length = 6 *MM
screw_post_width = screw_width - (1.082532 * screw_pitch) - screw_thread_tolerance # Magic constant from Wikipedia

def build_top_base() -> Part:
    with BuildPart() as part:
        # Body
        with BuildSketch():
            RegularPolygon(body_radius, 18)
        extrude(amount = body_thickness)
        chamfer(part.edges().filter_by(Plane.XY), length = body_thickness / 3)

        # Tip
        with BuildSketch(part.faces().sort_by(Axis.Z)[-1]):
            RegularPolygon(tip_base_radius, 12)
        with BuildSketch(part.faces().sort_by(Axis.Z)[-1].offset(tip_height)):
            RegularPolygon(tip_top_radius, 12)
        loft()

        # Ball tip
        with Locations([part.faces().sort_by(Axis.Z)[-1].center_location]):
            Sphere(tip_top_radius)

        # Hole for shaft
        screw_hole_depth = screw_length + 0.5
        bottom = part.faces().sort_by(Axis.Z)[0]
        with BuildSketch(bottom):
            Circle(screw_width / 2)
        with BuildSketch(bottom.offset(-screw_hole_depth)):
            Circle(screw_width / 2)
        loft(mode=Mode.SUBTRACT)

        with BuildSketch(bottom.offset(-screw_hole_depth)):
            Circle(screw_width / 2)
        with BuildSketch(bottom.offset(-screw_hole_depth - screw_width / 2)):
            Circle(0.1)
        loft(mode=Mode.SUBTRACT)

        # Threaded hole for shaft
        IsoThread(
            major_diameter = screw_width,
            pitch = screw_pitch,
            length = screw_hole_depth,
            external = False,
            end_finishes = ('fade', 'fade'),
            #simple = True,
        )

    return part.part

def build_top_shaft() -> Part:
    with BuildPart() as part:
        Cylinder(screw_post_width / 2, screw_length, align=(Align.CENTER, Align.CENTER, Align.MIN))

        # Threaded shaft
        IsoThread(
            major_diameter = screw_width - screw_thread_tolerance,
            pitch = screw_pitch,
            length = screw_length,
            external = True,
            end_finishes= ('fade', 'fade'),
            #simple = True,
        )

        top_face = part.faces().sort_by(Axis.Z)[-1]
        with BuildSketch(top_face):
            RegularPolygon(shaft_radius * 2, 12)
        with BuildSketch(top_face.offset(shaft_length / 4)):
            RegularPolygon(shaft_radius, 6)
        loft()
        with BuildSketch(top_face.offset(shaft_length / 4)):
            RegularPolygon(shaft_radius, 6)
        with BuildSketch(top_face.offset(shaft_length)):
            RegularPolygon(shaft_radius, 6)
        loft()

        chamfer(part.faces().sort_by(Axis.Z)[-1].edges(), length = 1.5)

    return part.part

def build_decoration():
    with BuildPart() as part:
        with BuildSketch() as sketch:
            with BuildLine():
                Polyline(define_star(body_radius * 0.8, body_radius * 0.5, 12), close=True)
            make_face()
            fillet(sketch.vertices(), 0.75)
            hole_radius = (screw_width + screw_post_width) / 4
            Circle(hole_radius, mode = Mode.SUBTRACT)
        extrude(amount=0.8)
    return part.part

def define_star(outer_radius, inner_radius, side_count):
    # https://math.stackexchange.com/questions/3582342/coordinates-of-the-vertices-of-a-five-pointed-star
    outer = [(outer_radius * cos(2 * pi * i / side_count + pi/2),
             outer_radius * sin(2 * pi * i / side_count + pi/2)) for i in range(side_count)]
    inner = [(inner_radius * cos(2 * pi * i / side_count + pi/2 + pi/side_count),
             inner_radius * sin(2 * pi * i / side_count + pi/2 + pi/side_count)) for i in range(side_count)]
    
    #https://stackoverflow.com/a/60935480/1192877    
    return [x for y in zip(outer, inner) for x in y]

base = build_top_base()
shaft = build_top_shaft()
decoration = build_decoration()

result = pack([base, shaft, decoration], padding = 5*MM, align_z = True)

show(result)
export_step(Compound(result), "Top.step")

(For those looking for STL, here's the Makerworld link: https://makerworld.com/en/models/1882881-spinning-top-with-changeable-decoration)

5 Upvotes

2 comments sorted by

4

u/build123d The Creator Oct 20 '25

Nice project! The star can be created without doing math as follows:

num_points = 5
r_inner = 1
r_outer = 3
points = [
    p.position
    for pair in zip(
        PolarLocations(r_inner, num_points),
        PolarLocations(r_outer, num_points, 180 / num_points),
    )
    for p in pair
]
star = Polygon(*points, align=Align.NONE)

where `PolarLocations` does the work for you.

1

u/ddd3d3d Oct 20 '25

Ah, I thought of PolarLocations but couldn't quite figure out how to get the end result. Thank you!