Build123d Workflow Examples

Simple gears on a backplate

Various properties and methods are made available in the class GearInfoMixin. The following example demonstrates the creation of a gear-pair and attaching them to a base-plate:

token gears_on_plate

Highlights:

  • You can use center_location_bottom, center_location_top, face_location_bottom, face_location_bottom to align parts with gear centers.

  • Note that center refers to the pitch circle center and face refers to the face (surface) of the gear. These are different for bevel gears.

  • gear.center is a numpy array, often needs to be converted to a Vector for build123d via np2v().

  • Ideal center distance can be retrieved after calling the mesh_to() method, and calculating the difference of gear.center values.

import py_gearworks as pgw
from build123d import *
from ocp_vscode import *


# this example demonstrates how to create a simple pair of gears and
# add additional features to them, and then assemble them on a baseplate
gearmodule = 2
gearheight = 20
bore_diameter = 5
pin_diameter = 2
sleeve_height = 7
sleeve_thickness = 3

gear1 = pgw.HelicalGear(
    number_of_teeth=13, module=gearmodule, height=gearheight, helix_angle=pgw.PI / 12
)
gear2 = pgw.HelicalGear(
    number_of_teeth=31, module=gearmodule, height=gearheight, helix_angle=-pgw.PI / 12
)
gear1.mesh_to(gear2, target_dir=pgw.DOWN, backlash=0.1, angle_bias=1)

# py_gearworks uses numpy arrays for vectors, build123d uses its own Vector class
# np2v() is shorthand for nppoint2Vector(), which makes the conversion
gear1_center_vector = pgw.np2v(gear1.center)
gear2_center_vector = pgw.np2v(gear2.center)
axial_distance_vector = gear1_center_vector - gear2_center_vector

with BuildPart() as gear1_part:
    # creating gear part
    gear1.build_part()
    # note: gear1 is moved and rotated to be meshed with gear2 by the mesh_to() method
    # the alignment of the sleeve and pinhole may need to be adjusted
    with Locations((gear1.center_location_top)):
        # note: location of top-center is aligned with tooth no. 0 of the gear
        # the angle is changed from the mesh_to() method and the helix angle as well
        sleeve = Cylinder(
            radius=bore_diameter / 2 + sleeve_thickness,
            height=sleeve_height,
            align=(Align.CENTER, Align.CENTER, Align.MIN),
        )
        loc_pin_hole = Location(
            Vector(0, 0, sleeve_height - pin_diameter * 3 / 2),
            (0, 90, 0),
        )
        # Holes with depth=None mean through all the way
        Hole(bore_diameter / 2, depth=None)
        with Locations([loc_pin_hole]):
            Hole(pin_diameter / 2, depth=None)
    # revolute joint seems fitting, but rigid could be used as well,
    # since gear rotation animation or simulation is not implemented
    RevoluteJoint(
        "gear_axis",
        axis=Axis(gear1_center_vector, (0, 0, 1)),
        angular_range=(-360, 360),
    )

with BuildPart() as gear2_part:
    gear2.build_part()
    with Locations((gear2.center_location_top)):
        # note: location of top-center is aligned with tooth no. 0 of the gear
        # the angle is changed from the helix angle
        Cylinder(
            radius=bore_diameter / 2 + sleeve_thickness,
            height=sleeve_height,
            align=(Align.CENTER, Align.CENTER, Align.MIN),
        )
        loc_pin_hole = Location(
            Vector(0, 0, sleeve_height - pin_diameter * 3 / 2),
            (0, 90, 0),
        )
        # Holes with depth=None mean through all the way
        Hole(bore_diameter / 2, depth=None)
        with Locations([loc_pin_hole]):
            Hole(pin_diameter / 2, depth=None)

    RevoluteJoint(
        "gear_axis",
        axis=Axis(gear2_center_vector, (0, 0, 1)),
        angular_range=(-360, 360),
    )


with BuildPart() as baseplate:
    box = Box(100, 10, 50)
    face = box.faces().sort_by(Axis.Y)[0]
    # note: the orientation of the face is such that the local Y aligns with global X
    loc = face.center_location
    # mult operation on locations means locate 2nd location within 1st location
    loc_g1 = loc * Location(axial_distance_vector * 0.5)
    loc_g2 = loc * Location(-axial_distance_vector * 0.5)
    with Locations([loc_g1, loc_g2]):
        Hole(bore_diameter / 2, depth=50)
    # joints don't seem to work well with Locations context manager
    # so they are created outside of it with joint_location specified as kwarg

    # build123d joint system needs pairs of rigid-revolute joints,
    # revolute-revolute pair does not work
    RigidJoint("gear1_axis", joint_location=loc_g1)
    RigidJoint("gear2_axis", joint_location=loc_g2)


baseplate.joints["gear1_axis"].connect_to(gear1_part.joints["gear_axis"])
baseplate.joints["gear2_axis"].connect_to(gear2_part.joints["gear_axis"])

show_all(render_joints=True)

Crescent Gear Pump

This example demonstrates building a gear pump. The design is missing fasteners and seals, but showcases the gear generator and its helper functions for build-123d workflow.

Highlights:

  • You can use center_location_bottom and center_location_top to align parts with gear centers.

  • The radii_data_top method generates reference curves for the gear.

  • The LineOfAction class is available for generating the line of action between gears.

  • Sometimes converter functions are needed such as arc_to_b123d() and line_to_b123d(). These convert between py_gearworks’ own geometry classes and build123d geometry.

  • Animation is used from ocp_vscode to visualize the gear meshing. Animation can be non-intuitive, but explaining it is beyond this example.

token gearpump_1
import py_gearworks as pgw
from build123d import *
from ocp_vscode import *
import numpy as np

set_port(3939)

gearheight = 10
axis_diameter = 6
port_diameter = 5
gearmodule = 2
wall_thickness = 3
clearence = 0.1

gear1 = pgw.SpurGear(
    number_of_teeth=17,
    module=gearmodule,
    height=gearheight,
    addendum_coefficient=1.2,
    profile_shift=0.2,
    z_anchor=0.5,
)
gear2 = pgw.SpurRingGear(
    number_of_teeth=23,
    module=gearmodule,
    height=gearheight,
    addendum_coefficient=1.4,
    dedendum_coefficient=0.8,
    outside_ring_coefficient=2.2,
    profile_shift=0.2,
    z_anchor=0.5,
    # I used the angle kwarg to iteratively check for interference
    angle=0.135,
)


with BuildPart() as gearpart1:
    gear1.build_part()
    with Locations((gear1.center_location_bottom)):
        # notch
        # a rectangular hole on the radius in Y direction
        with Locations((0, axis_diameter / 2, 0)):
            Box(
                length=3,
                width=2,
                height=gearheight,
                mode=Mode.SUBTRACT,
                # location is on the bottom of gear, need to align Z to with MIN
                align=(Align.CENTER, Align.CENTER, Align.MIN),
            )
        # axle hole
        Hole(radius=axis_diameter / 2)

# backlash is specified as a coefficient of module
# clearence is in absolute units, so convert to coefficient of module
gear1.mesh_to(gear2, backlash=clearence / gearmodule, angle_bias=1.0)

gearpart1.part.label = "gear1"
gearpart1.part.location = gear1.center_location_middle


# ring gear needs no modifications
with BuildPart() as gearpart2:
    gear2.build_part()

gearpart2.part.label = "gear2"
# set up rendering colors
gearpart1.part.color = (0.75, 0.75, 0.75)
gearpart2.part.color = (0.6, 0.6, 0.6)


with BuildPart() as housing_base:
    r_outer_gear2 = gear2.max_outside_radius + clearence
    r_outer_wall = r_outer_gear2 + wall_thickness
    # External housing with even-ish wall thickness
    Cylinder(
        radius=r_outer_wall,
        height=gearheight + wall_thickness * 2,
        mode=Mode.ADD,
    )
    Cylinder(radius=r_outer_gear2, height=gearheight, mode=Mode.SUBTRACT)

# split housing base to top and bottom parts
with BuildPart() as housing_bottom:
    add(housing_base.part.split(tool=Plane.XY, keep=Keep.BOTTOM))
    # axle hole
    with Locations((gear1.center_location_bottom)):
        Hole(radius=axis_diameter / 2)
housing_bottom.part.label = "housing_bottom"

with BuildPart() as crescent:
    with BuildSketch():
        # crescent constructed from ring gear inner (dedendum) circle and
        # gear1 outer (addendum) circle.
        with Locations((gear2.center_location_bottom)):
            Circle(radius=gear2.dedendum_radius - clearence, mode=Mode.ADD)
        with Locations((gear1.center_location_bottom)):
            Circle(radius=gear1.addendum_radius + clearence, mode=Mode.SUBTRACT)
        # cut off the right side sharp tips of crescent
        Rectangle(
            width=gear2.addendum_radius,
            height=2 * gear2.addendum_radius,
            mode=Mode.SUBTRACT,
            align=(Align.MIN, Align.CENTER),
        )
        # fillet for good measure
        fillet(vertices(), radius=1)
    extrude(amount=gearheight / 2, both=True)

crescent.part.label = "crescent"
crescent.part.color = (0.5, 0.5, 0.8)


# indicator sketches
addendum_circle_1 = pgw.arc_to_b123d(gear1.radii_data_top.r_a_curve)
addendum_circle_2 = pgw.arc_to_b123d(gear2.radii_data_top.r_a_curve)

# dedendum sketches
dedendum_circle_1 = pgw.arc_to_b123d(gear1.radii_data_top.r_d_curve)
dedendum_circle_2 = pgw.arc_to_b123d(gear2.radii_data_top.r_d_curve)

# involute base circle is not in the radii data
# because radii data was meant to be generic and apply to other gears
base_circle_1 = pgw.arc_to_b123d(gear1.circle_involute_base(z_ratio=1))
base_circle_2 = pgw.arc_to_b123d(gear2.circle_involute_base(z_ratio=1))

loa1, loa2 = pgw.LineOfAction(gear2, gear1, z_ratio=1).LOA_gen()
line_of_action_1 = pgw.line_to_b123d(loa1)
line_of_action_2 = pgw.line_to_b123d(loa2)

# coloring
line_of_action_1.color = (1, 0.2, 0.2)
line_of_action_2.color = (1, 0.2, 0.2)
base_circle_1.color = (0, 0, 0)
addendum_circle_1.color = (0, 0, 0)

# construction of the housing top with channel volumes for oil-flow
channel_thickness = 3
# blocker width is aligned with the distance between the ends of the 2 lines of action
# this is not official pump design advice
blocker_width = min(
    (line_of_action_1 @ 1 - line_of_action_2 @ 1).length,
    (line_of_action_1 @ 0 - line_of_action_2 @ 0).length,
)

with BuildPart() as housing_top:
    add(housing_base.part.split(tool=Plane.XY, keep=Keep.TOP))

    # main cavity + horizontal blocker
    # the top_position rotates with the gear, but we only need the position here
    with Locations(Location(gear2.center_location_top.position)):
        Cylinder(
            radius=r_outer_wall,
            height=channel_thickness + wall_thickness,
            mode=Mode.ADD,
            align=(Align.CENTER, Align.CENTER, Align.MIN),
        )
        Cylinder(
            radius=gear2.addendum_radius,
            height=channel_thickness,
            mode=Mode.SUBTRACT,
            align=(Align.CENTER, Align.CENTER, Align.MIN),
        )
        # horizontal blocker
        Box(
            length=gear2.addendum_radius * 2,
            width=blocker_width,
            height=channel_thickness,
            mode=Mode.ADD,
            align=(Align.CENTER, Align.CENTER, Align.MIN),
        )
    # axle support
    # top location is aligned with gear rotation, but only using position here
    with Locations(Location(gear1.center_location_top.position)):
        r = axis_diameter / 2 + wall_thickness
        Cylinder(
            radius=r,
            height=channel_thickness,
            mode=Mode.ADD,
            align=(Align.CENTER, Align.CENTER, Align.MIN),
        )
        Box(
            length=2 * r,
            width=2 * r,
            height=channel_thickness,
            mode=Mode.ADD,
            align=(Align.MAX, Align.CENTER, Align.MIN),
        )
    with BuildSketch(Location(gear2.center_location_top.position)):
        Circle(radius=gear2.addendum_radius, mode=Mode.ADD)
        Rectangle(
            width=gear2.addendum_radius,
            height=gear2.addendum_radius * 2,
            align=(Align.MAX, Align.CENTER),
            mode=Mode.INTERSECT,
        )
    extrude(amount=channel_thickness)
    r_hole = (gear1.addendum_radius + gear2.dedendum_radius) / 2
    ax_offs = (gear1.center - gear2.center)[0]
    with Locations([(ax_offs / 2, r_hole, 0), (ax_offs / 2, -r_hole, 0)]):
        Hole(radius=port_diameter / 2)

    with Locations(Location(gear1.center_location_top)):
        # axle pocket, should not go all the way through
        Cylinder(
            radius=axis_diameter / 2,
            height=channel_thickness,
            mode=Mode.SUBTRACT,
            align=(Align.CENTER, Align.CENTER, Align.MIN),
        )
housing_top.part.label = "housing_top"
anim_collector = Compound(
    children=(
        [
            gearpart1.part,
            gearpart2.part,
            housing_bottom.part,
            crescent.part,
            housing_top.part,
            line_of_action_1,
            line_of_action_2,
            addendum_circle_1,
            dedendum_circle_2,
        ]
    ),
    label="assembly",
)

duration = 2
n = duration * 30
time_track = np.linspace(0, duration, n + 1)
gear1_track = np.linspace(0, gear1.pitch_angle * 180 / np.pi, n + 1) * duration
gear2_track = np.linspace(0, gear2.pitch_angle * 180 / np.pi, n + 1) * duration
animation1 = Animation(anim_collector)
animation1.add_track("/assembly/gear1", "rz", time_track, gear1_track)
animation1.add_track("/assembly/gear2", "rz", time_track, gear2_track)
show(anim_collector)
animation1.animate(speed=1)

Here you can find the sketches that helped the construction of the crescent and the fluid channels.

token gearpump_with_circles