Ejemplo n.º 1
0
 def resulting_plane(shape0):
     p0 = resulting_pln(shape0)
     return cq.Plane(
         cq.Vector(p0.Location()),
         cq.Vector(p0.XAxis().Direction()),
         cq.Vector(p0.Axis().Direction()),
     )
def make_line(start, direction):
    """Single linear edge in a Wire, as an indicator"""
    start_v = cadquery.Vector(*start)
    finish_v = start_v.add(cadquery.Vector(*direction))
    edge = cadquery.Edge.makeLine(start_v, finish_v)
    wire = cadquery.Wire.assembleEdges([edge])
    return cadquery.Workplane('XY').newObject([wire])
Ejemplo n.º 3
0
 def get_ball_torus(cls, rolling_radius, ball_radius):
     return cadquery.Workplane("XY").union(
         cadquery.CQ(cadquery.Solid.makeTorus(
             rolling_radius, ball_radius, # radii
             pnt=cadquery.Vector(0,0,0).wrapped,
             dir=cadquery.Vector(0,0,1).wrapped,
             angleDegrees1=0.,
             angleDegrees2=360.,
         ))
     )
def generate_part(num_pins):
    calc_dim = dimensions(num_pins)
    pins = generate_pins(num_pins)
    body, insert = generate_body(num_pins, calc_dim)

    # adjust for matching KiCad expectation
    body = body.rotate((0, 0, 0),(0, 0, 1), 180).translate(cq.Vector(calc_dim.length-series_params.pin_inside_distance,series_params.pin_xpos,0))
    pins = pins.rotate((0, 0, 0),(0, 0, 1), 180).translate(cq.Vector(calc_dim.length-series_params.pin_inside_distance,series_params.pin_xpos,0))

    return (body, pins)
    def test_fastener(self):
        obj = FastenedAssembly()
        bolt = obj.find('fastener.bolt')
        nut = obj.find('fastener.nut')

        self.assertEquals(bolt.world_coords.origin, cadquery.Vector(
            (1, 2, 30)))
        self.assertGreater(bolt.bounding_box.zlen,
                           obj.find('top').height + obj.find('base').height)
        self.assertEquals(nut.world_coords.origin, cadquery.Vector((1, 2, 0)))
Ejemplo n.º 6
0
def simple_assy():

    b1 = cq.Solid.makeBox(1, 1, 1)
    b2 = cq.Workplane().box(1, 1, 2)
    b3 = cq.Workplane().pushPoints([(0, 0), (-2, -5)]).box(1, 1, 3)

    assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(2, -5, 0)))
    assy.add(b2, loc=cq.Location(cq.Vector(1, 1, 0)))
    assy.add(b3, loc=cq.Location(cq.Vector(2, 3, 0)))

    return assy
Ejemplo n.º 7
0
def nested_assy_sphere():

    b1 = cq.Workplane().box(1, 1, 1).faces("<Z").tag("top_face").end()
    b2 = cq.Workplane().box(1, 1, 1).faces("<Z").tag("bottom_face").end()
    b3 = cq.Workplane().pushPoints([(-2, 0), (2, 0)]).tag("pts").sphere(1).tag("boxes")

    assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(0, 0, 0)), name="TOP")
    assy2 = cq.Assembly(b2, loc=cq.Location(cq.Vector(0, 4, 0)), name="SECOND")
    assy2.add(b3, loc=cq.Location(cq.Vector(0, 4, 0)), name="BOTTOM")

    assy.add(assy2, color=cq.Color("green"))

    return assy
Ejemplo n.º 8
0
def nested_assy():

    b1 = cq.Workplane().box(1, 1, 1)
    b2 = cq.Workplane().box(1, 1, 1)
    b3 = cq.Workplane().pushPoints([(-2, 0), (2, 0)]).box(1, 1, 0.5)

    assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(0, 0, 0)), name="TOP")
    assy2 = cq.Assembly(b2, loc=cq.Location(cq.Vector(0, 4, 0)), name="SECOND")
    assy2.add(b3, loc=cq.Location(cq.Vector(0, 4, 0)), name="BOTTOM")

    assy.add(assy2, color=cq.Color("green"))

    return assy
    def make(self):
        cone_radius = self.diameter / 2
        cone_height = cone_radius  # to achieve a 45deg angle
        cylinder_radius = cone_radius - self.chamfer
        cylinder_height = self.height
        shaft_radius = (self.diameter / 2.) - self.height

        cone = cadquery.Workplane("XY").union(
            cadquery.CQ(cadquery.Solid.makeCone(0, cone_radius, cone_height)) \
                .translate((0, 0, -cone_height))
        )

        cylinder = cadquery.Workplane("XY") \
            .circle(cylinder_radius).extrude(-cylinder_height)

        head = cone.intersect(cylinder)

        # Raised bubble (if given)
        if self.raised:
            sphere_radius = ((self.raised ** 2) + (cylinder_radius ** 2)) / (2 * self.raised)

            sphere = cadquery.Workplane("XY").workplane(offset=-(sphere_radius - self.raised)) \
                .sphere(sphere_radius)
            raised_cylinder = cadquery.Workplane("XY").circle(cylinder_radius).extrude(self.raised)
            from Helpers import show

            raised_bubble = sphere.intersect(raised_cylinder)
            head = head.union(raised_bubble)

        # Bugle Head
        if self.bugle and (0 <= self.bugle_ratio < 1.0):
            # bugle_angle = angle head material makes with chamfer cylinder on top
            bugle_angle = (pi / 4) * self.bugle_ratio
            # face_span = longest straight distance along flat conical face (excluding chamfer)
            face_span = sqrt(2) * (((self.diameter / 2.) - self.chamfer) - shaft_radius)
            r2 = (face_span / 2.) / sin((pi / 4) - bugle_angle)
            d_height = r2 * sin(bugle_angle)
            r1 = (r2 * cos(bugle_angle)) + shaft_radius

            torus = cadquery.Workplane("XY").union(
                cadquery.CQ(cadquery.Solid.makeTorus(
                    r1, r2, # radii
                    pnt=cadquery.Vector(0,0,0),
                    dir=cadquery.Vector(0,0,1),
                    angleDegrees1=0.,
                    angleDegrees2=360.
                )).translate((0, 0, -(self.height + d_height)))
            )
            head = head.cut(torus)

        return head
def face_from_points(points):
    # print('face_from_points()')
    edges = []
    num_pnts = len(points)
    for i in range(len(points)):
        p1 = points[i]
        p2 = points[(i + 1) % num_pnts]
        edges.append(
            cq.Edge.makeLine(
                cq.Vector(p1[0], p1[1], p1[2]),
                cq.Vector(p2[0], p2[1], p2[2]),
            ))

    face = cq.Face.makeFromWires(cq.Wire.assembleEdges(edges))

    return face
Ejemplo n.º 11
0
    def __render_outline(self, workplane, d, outlineArray, offset):

        outline = cq.Workplane("XY")
        arcs = []

        for i in range(0, len(outlineArray)):
            last_drill = outlineArray[(i-1) % len(outlineArray)]
            this_drill = outlineArray[i]
            next_drill = outlineArray[(i+1) % len(outlineArray)]

            edge_ab = primitives.edge(self.__keyloc_to_xy_mm(last_drill["position"].x, last_drill["position"].y, last_drill["offsetByHandAngle"]), self.__keyloc_to_xy_mm(this_drill["position"].x, this_drill["position"].y, this_drill["offsetByHandAngle"]))
            edge_bc = primitives.edge(self.__keyloc_to_xy_mm(this_drill["position"].x, this_drill["position"].y, this_drill["offsetByHandAngle"]), self.__keyloc_to_xy_mm(next_drill["position"].x, next_drill["position"].y, next_drill["offsetByHandAngle"]))

            # Work out the offset from the centre, if overridden
            this_offset = this_drill["radius"] if "radius" in this_drill else offset

            # Flip convex/concave if we're generating an internal contour
            convex = this_drill["convex"] if this_offset > 0 else not this_drill["convex"]

            this_arc = primitives.arc(edge_ab, edge_bc, this_offset, convex)
            if this_arc.chord_length > 1e-9:
                arcs.append(this_arc)

        last_arc = arcs[-1]
        outline = outline.moveTo(primitives.mm2m(last_arc.end.x), primitives.mm2m(last_arc.end.y))
        for this_arc in arcs:
            outline = outline.lineTo(primitives.mm2m(this_arc.start.x), primitives.mm2m(this_arc.start.y))
            outline = outline.tangentArcPoint(cq.Vector(primitives.mm2m(this_arc.end.x), primitives.mm2m(this_arc.end.y)), relative=False)

            if bool(d["locations"]):
                self.__circle_at_xy(workplane, this_arc.centre, self.__locationFiducialSize)

        workplane.add(outline.wire().combine())
Ejemplo n.º 12
0
    def make(self):

        # cone radii
        cone_radius_at = lambda z: self.roller_surface_radius + (
            self.roller_surface_gradient * z)
        cone_radius_base = cone_radius_at(self.base_height)
        cone_radius_top = cone_radius_at(self.base_height + self.height)

        # ring base shape
        outer_ring = cadquery.Workplane('XY', origin=(0, 0, self.base_height)) \
            .circle(self.outer_diam / 2).extrude(self.height)

        # cut cone from base shape (provides conical rolling surface)
        if abs(self.roller_surface_gradient) > 1e-6:
            cone = cadquery.CQ(
                cadquery.Solid.makeCone(
                    radius1=cone_radius_base,
                    radius2=cone_radius_top,
                    height=self.height,
                    dir=cadquery.Vector(0, 0, 1),
                )).translate((0, 0, self.base_height))
            outer_ring = outer_ring.cut(cone)
        else:
            outer_ring = outer_ring.faces('>Z') \
                .circle(self.roller_surface_radius).cutThruAll()

        return outer_ring
Ejemplo n.º 13
0
def create_hexapod():
    # Some shortcuts
    L = lambda *args: cq.Location(cq.Vector(*args))
    C = lambda *args: cq.Color(*args)

    # Leg assembly
    leg = MAssembly(upper_leg, name="upper",
                    color=C("orange")).add(lower_leg,
                                           name="lower",
                                           color=C("orange"),
                                           loc=L(80, 0, 0))
    # Hexapod assembly
    hexapod = (MAssembly(
        base, name="bottom", color=C("gray"),
        loc=L(0, 1.1 * width,
              0)).add(base,
                      name="top",
                      color=C(0.9, 0.9, 0.9),
                      loc=L(0, -2.2 * width,
                            0)).add(stand,
                                    name="front_stand",
                                    color=C(0.5, 0.8, 0.9),
                                    loc=L(40, 100,
                                          0)).add(stand,
                                                  name="back_stand",
                                                  color=C(0.5, 0.8, 0.9),
                                                  loc=L(-40, 100, 0)))

    for i, name in enumerate(leg_names):
        hexapod.add(leg, name=name, loc=L(100, -55 * (i - 1.7), 0))

    return hexapod
Ejemplo n.º 14
0
 def bolthole(location, diameter, clamp_length, nuthole_size,
              nuthole_depth):
     """
     Create a bolthole at the specified location in the current workplane, with the given 
     measures.
     :param location: The location to place the bolthole, using the point at the center of 
         the hole cross-section and at half its clamp length as the handle.
     :param diameter: Diameter of the cylindrical section of the bolthole.
     :param clamp_length: Length between start of the bolt head and start of the nuthole.
     :param nuthole_size: Size between flats of a hexagonal hole for a nut.
     :param nuthole_depth: Maximum depth of the nuthole. If the part ends earlier, this 
         depth is not reached.
     """
     bolthole = (cq.Workplane().bolt(
         bolt_size=diameter,
         head_size=2 * diameter,
         head_length=2 * diameter,
         head_shape="cylindrical",
         head_angle=90,
         clamp_length=clamp_length,
         nut_size=nuthole_size,
         nut_length=nuthole_depth).val().located(
             location *
             cq.Location(cq.Vector(0, 0, -clamp_length / 2))))
     # show_object(bolthole) # Debug helper.
     return bolthole
Ejemplo n.º 15
0
def metadata_assy():

    b1 = cq.Solid.makeBox(1, 1, 1)
    b2 = cq.Workplane().box(1, 1, 2)

    assy = cq.Assembly(
        b1,
        loc=cq.Location(cq.Vector(2, -5, 0)),
        name="base",
        metadata={"b1": "base-data"},
    )
    sub_assy = cq.Assembly(
        b2, loc=cq.Location(cq.Vector(1, 1, 1)), name="sub", metadata={"b2": "sub-data"}
    )
    assy.add(sub_assy)
    return assy
Ejemplo n.º 16
0
 def __init__(self, origin, along_axis, radius, debug=False):
     self.outer_radius = radius
     self.axis = self.get_axis(along_axis)
     xdir = self.get_ortho_vector(self.axis)
     self.base = cq.Plane(cq.Vector(origin), xdir, self.axis.toTuple())
     if debug:
         utils.make_debug_cylinder(self.base, self.outer_radius)
Ejemplo n.º 17
0
    def __init__(self, workplane, measures):
        """
        A parametric, grooved wall element that can be integrated into panel walls.

        .. todo: Since this is not a part that can be used as it is, it should rather be 
            implemented as a CadQuery plugin.
        .. todo:: Parameter documentation.
        .. todo:: Add parameters for edge and corner rounding.
        """

        self.model = workplane
        self.measures = measures

        # Add optional measures if missing, using their default values.
        if not hasattr(measures, 'center_offset'):
            measures.center_offset = cq.Vector(0, 0, 0)
        if not hasattr(measures, 'grooves'): measures.grooves = Measures()
        if not hasattr(measures.grooves, 'left'): measures.grooves.left = False
        if not hasattr(measures.grooves, 'right'):
            measures.grooves.right = False
        if not hasattr(measures.grooves, 'top'): measures.grooves.top = False
        if not hasattr(measures.grooves, 'bottom'):
            measures.grooves.bottom = False

        self.build()
Ejemplo n.º 18
0
    def make(self):

        cone_radius_at = lambda z: self.roller_surface_radius + (
            self.roller_surface_gradient * z)
        cone_radius_base = cone_radius_at(self.base_height)
        cone_radius_top = cone_radius_at(self.base_height + self.height)

        # ring base shape
        inner_ring = cadquery.Workplane('XY', origin=(0, 0, self.base_height)) \
            .circle(max(cone_radius_base, cone_radius_top)) \
            .circle(self.inner_diam / 2) \
            .extrude(self.height)

        # intersect cone with base shape (provides conical rolling surface)
        if abs(self.roller_surface_gradient) > 1e-6:
            cone = cadquery.CQ(
                cadquery.Solid.makeCone(
                    radius1=cone_radius_base,
                    radius2=cone_radius_top,
                    height=self.height,
                    dir=cadquery.Vector(0, 0, 1),
                )).translate((0, 0, self.base_height))
            inner_ring = inner_ring.intersect(cone)

        return inner_ring
Ejemplo n.º 19
0
def radial_holes_type(diameter, depth, size, n, offset, hole_type):
    holes = []
    if (hole_type == 'thru'):
        [screw_diameter, length] = thru(size, 1)
    if (hole_type == 'tap'):
        [screw_diameter, length] = tap(size, 1)
    for index in range(n):
        theta = 360.0 * index / n + offset
        x = diameter / 2.0 * math.cos(theta / 180.0 * math.pi)
        y = diameter / 2.0 * math.sin(theta / 180.0 * math.pi)
        p = cq.Vector(x, y, 0)
        d = cq.Vector(-x, -y, 0)
        # print(theta, p,d)
        hole = cq.Solid.makeCylinder(screw_diameter / 2, depth, pnt=p, dir=d)
        holes.append(hole)
    return holes
def sa_cap(Usize=1):
    # MODIFIED TO NOT HAVE THE ROTATION.  NEEDS ROTATION DURING ASSEMBLY
    sa_length = 18.25

    bw2 = Usize * sa_length / 2
    bl2 = sa_length / 2
    m = 0
    pw2 = 6 * Usize + 1
    pl2 = 6

    if Usize == 1:
        m = 17 / 2

    k1 = cq.Workplane('XY').polyline([(bw2, bl2), (bw2, -bl2), (-bw2, -bl2),
                                      (-bw2, bl2), (bw2, bl2)])
    k1 = cq.Wire.assembleEdges(k1.edges().objects)
    k1 = cq.Workplane('XY').add(
        cq.Solid.extrudeLinear(outerWire=k1,
                               innerWires=[],
                               vecNormal=cq.Vector(0, 0, 0.1)))
    k1 = k1.translate((0, 0, 0.05))
    k2 = cq.Workplane('XY').polyline([(pw2, pl2), (pw2, -pl2), (-pw2, -pl2),
                                      (-pw2, pl2), (pw2, pl2)])
    k2 = cq.Wire.assembleEdges(k2.edges().objects)
    k2 = cq.Workplane('XY').add(
        cq.Solid.extrudeLinear(outerWire=k2,
                               innerWires=[],
                               vecNormal=cq.Vector(0, 0, 0.1)))
    k2 = k2.translate((0, 0, 12.0))
    if m > 0:
        m1 = cq.Workplane('XY').polyline([(m, m), (m, -m), (-m, -m), (-m, m),
                                          (m, m)])
        m1 = cq.Wire.assembleEdges(m1.edges().objects)
        m1 = cq.Workplane('XY').add(
            cq.Solid.extrudeLinear(outerWire=m1,
                                   innerWires=[],
                                   vecNormal=cq.Vector(0, 0, 0.1)))
        m1 = m1.translate((0, 0, 6.0))
        key_cap = hull_from_shapes((k1, k2, m1))
    else:
        key_cap = hull_from_shapes((k1, k2))

    key_cap = key_cap.translate((0, 0, 5 + plate_thickness))
    # key_cap = key_cap.color((220 / 255, 163 / 255, 163 / 255, 1))

    return key_cap
Ejemplo n.º 21
0
def test_hull():

    c1 = cq.Edge.makeCircle(0.5, (-1.5, 0.5, 0))
    c2 = cq.Edge.makeCircle(0.5, (1.9, 0.0, 0))
    c3 = cq.Edge.makeCircle(0.2, (0.3, 1.5, 0))
    c4 = cq.Edge.makeCircle(0.2, (1.0, 1.5, 0))
    c5 = cq.Edge.makeCircle(0.1, (0.0, 0.0, 0.0))
    e1 = cq.Edge.makeLine(cq.Vector(0, -0.5), cq.Vector(-0.5, 1.5))
    e2 = cq.Edge.makeLine(cq.Vector(2.1, 1.5), cq.Vector(2.6, 1.5))

    edges = [c1, c2, c3, c4, c5, e1, e2]

    h = hull.find_hull(edges)

    assert len(h.Vertices()) == 11
    assert h.IsClosed()
    assert h.isValid()
Ejemplo n.º 22
0
 def test_fastener(self):
     obj = FastenedAssembly()
     screw = obj.find('fastener.screw')
     self.assertEquals(screw.world_coords.origin, cadquery.Vector(
         (1, 2, 30)))
     self.assertGreater(screw.bounding_box.zlen, obj.find('base').height)
     self.assertLess(screw.bounding_box.zlen,
                     obj.find('top').height + obj.find('base').height)
Ejemplo n.º 23
0
def create_leg(x, y):
    L = lambda *args: cq.Location(cq.Vector(*args))
    C = lambda *args: cq.Color(*args)

    leg = MAssembly(cq.Workplane("YZ").polyline([(0, 0), (x, 0), (x, y)]), name="base", color=C("Gray"))
    for i, name in enumerate(link_list):
        leg.add(parts[name], name=name, color=C(links[name]["col"]), loc=L(0, 0, i * 10 - 50))
    return leg
Ejemplo n.º 24
0
def test_infinite_face_constraint_PointInPlane(origin, normal):
    """
    An OCCT infinite face has a center at (1e99, 1e99), but when a user uses it
    in a constraint, the center should be basePnt.
    """

    f0 = cq.Face.makePlane(length=None, width=None, basePnt=origin, dir=normal)

    c0 = cq.assembly.Constraint(
        ("point", "plane"),
        (cq.Vertex.makeVertex(10, 10, 10), f0),
        sublocs=(cq.Location(), cq.Location()),
        kind="PointInPlane",
    )
    p0 = c0._getPln(c0.args[1])  # a gp_Pln
    derived_origin = cq.Vector(p0.Location())
    assert derived_origin == cq.Vector(origin)
Ejemplo n.º 25
0
        def shovel_profile(self, m):
            width = m.shovels.size
            # Since the two arcs are at an angle (see center_angle_rad), the minimum thickness
            # is less than m.shovels.cavity. TODO: Make this parametric in a reasonable way.
            depth = m.shovels.cavity * 2 - 3.0
            circumradius = m.baseplate.diameter / 2
            # Angle at which the outer extension of the shovel appears from the baseplate center.
            # TODO: Make the center angle configurable in a reasonable way. Currently it is
            # derived from the number of shovels using a statis factor (0.45).
            center_angle_rad = radians(360 / m.shovels.count) * 0.45

            # Profile corner points.
            left_bottom = cq.Vector((-width, -depth / 2))
            right_bottom = (
                (cq.Vector((circumradius, 0)) * -1) +
                (cq.Vector(cos(center_angle_rad / 2),
                           -sin(center_angle_rad / 2)) * circumradius))
            right_center = cq.Vector((0, 0))
            right_top = (cq.Vector((circumradius, 0)) * -1 +
                         cq.Vector(cos(center_angle_rad / 2),
                                   sin(center_angle_rad / 2)) * circumradius)
            left_top = cq.Vector((-width, depth / 2))

            profile = (self.moveTo(left_bottom.x, left_bottom.y).sagittaArc(
                right_bottom.toTuple2D(), m.shovels.cavity).threePointArc(
                    right_center.toTuple2D(),
                    right_top.toTuple2D()).sagittaArc(
                        left_top.toTuple2D(), m.shovels.cavity).close())

            return self.newObject(profile.objects)
Ejemplo n.º 26
0
def toLocalVector(self, x=0.0, y=0.0, z=0.0):
    if type(x) is cq.Workplane:
        x = x.first().val().Center().x
    if type(y) is cq.Workplane:
        y = y.first().val().Center().y
    if type(z) is cq.Workplane:
        z = z.first().val().Center().z

    return self.plane.toLocalCoords(cq.Vector(x, y, z))
Ejemplo n.º 27
0
    def _makeCross(loc):
        pnts = []
        t = 2 * pi / fn
        R = r1 / 2 / sin(t)
        for i in range(fn + 1):
            pts = [R * cos(i * t + pi / fn), R * sin(i * t + pi / fn)]
            pnts.append(cq.Vector(pts[0], pts[1], 0))

        return cq.Wire.makePolygon(pnts, forConstruction).locate(loc)
Ejemplo n.º 28
0
    def _one_heatsert(loc):
        pnt = cq.Vector(0, 0, 0)
        boreDir = cq.Vector(0, 0, -1)

        hole = cq.Solid.makeCylinder(diam / 2.0, depth, pnt, boreDir)

        if bolt_clear:
            extra_hole_diam = bolt_diam * 1.2
            extra_hole = cq.Solid.makeCylinder(extra_hole_diam / 2.0,
                                               bolt_clear, pnt, boreDir)
            hole = hole.fuse(extra_hole)

        if chamfer:
            cone_face_radius = diam / 2 + chamfer_vals[0]
            cone = cq.Solid.makeCone(cone_face_radius, diam / 2,
                                     chamfer_vals[1], pnt, boreDir)
            hole = hole.fuse(cone)

        return hole.move(loc)
Ejemplo n.º 29
0
def hull_bracket(angle, width, thickness, top_height, side_height,
                 top_hole_radius, top_hole_distance, top_hole_margin,
                 side_hole_radius, side_hole_distance, side_hole_margin):

    thm = top_height / 2 - top_hole_margin

    return (cq.Workplane("front").box(
        width, top_height, thickness).faces('>Z').pushPoints([
            (top_hole_distance / 2, thm), (-top_hole_distance / 2, thm)
        ]).circle(top_hole_radius).cutThruAll().faces(
            ">Y").workplane().transformed(
                offset=cq.Vector(
                    0, thickness / 2 - sin(radians(angle)) * thickness / 2,
                    cos(radians(angle)) * thickness / 2),
                rotate=cq.Vector(angle, 0, 0)).moveTo(0, side_height / 2).box(
                    width, side_height, thickness).faces('>Y').pushPoints([
                        (side_hole_distance / 2, side_hole_margin),
                        (-side_hole_distance / 2, side_hole_margin)
                    ]).circle(side_hole_radius).cutThruAll())
Ejemplo n.º 30
0
def simple_assy2():

    b1 = cq.Workplane().box(1, 1, 1)
    b2 = cq.Workplane().box(2, 1, 1)

    assy = cq.Assembly()
    assy.add(b1, name="b1")
    assy.add(b2, loc=cq.Location(cq.Vector(0, 0, 4)), name="b2")

    return assy