def testNthDistance(self):
        c = Workplane('XY').pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1)

        #2nd face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val()
        self.assertAlmostEqual(val.Center().x, -1.5)

        #2nd face with inversed selection vector
        val = c.faces(selectors.DirectionNthSelector(Vector(-1, 0, 0),
                                                     1)).val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        #2nd last face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0),
                                                     -2)).val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        #Last face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0),
                                                     -1)).val()
        self.assertAlmostEqual(val.Center().x, 2.5)

        #check if the selected face if normal to the specified Vector
        self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length,
                               0.0)

        #repeat the test using string based selector

        #2nd face
        val = c.faces('>(1,0,0)[1]').val()
        self.assertAlmostEqual(val.Center().x, -1.5)

        #2nd face with inversed selection vector
        val = c.faces('>(-1,0,0)[1]').val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        #2nd last face
        val = c.faces('>X[-2]').val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        #Last face
        val = c.faces('>X[-1]').val()
        self.assertAlmostEqual(val.Center().x, 2.5)

        #check if the selected face if normal to the specified Vector
        self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length,
                               0.0)
Ejemplo n.º 2
0
    def testNthDistance(self):
        c = Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1)

        # 2nd face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val()
        self.assertAlmostEqual(val.Center().x, -1.5)

        # 2nd face with inversed selection vector
        val = c.faces(selectors.DirectionNthSelector(Vector(-1, 0, 0),
                                                     1)).val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # 2nd last face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0),
                                                     -2)).val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # Last face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0),
                                                     -1)).val()
        self.assertAlmostEqual(val.Center().x, 2.5)

        # check if the selected face if normal to the specified Vector
        self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length,
                               0.0)

        # repeat the test using string based selector

        # 2nd face
        val = c.faces(">(1,0,0)[1]").val()
        self.assertAlmostEqual(val.Center().x, -1.5)
        val = c.faces(">X[1]").val()
        self.assertAlmostEqual(val.Center().x, -1.5)

        # 2nd face with inversed selection vector
        val = c.faces(">(-1,0,0)[1]").val()
        self.assertAlmostEqual(val.Center().x, 1.5)
        val = c.faces("<X[1]").val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # 2nd last face
        val = c.faces(">X[-2]").val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # Last face
        val = c.faces(">X[-1]").val()
        self.assertAlmostEqual(val.Center().x, 2.5)

        # check if the selected face if normal to the specified Vector
        self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length,
                               0.0)

        # test selection of multiple faces with the same distance
        c = (Workplane("XY").box(
            1, 4, 1, centered=(False, True, False)).faces("<Z").box(
                2, 2, 2, centered=(True, True, False)).faces(">Z").box(
                    1, 1, 1, centered=(True, True, False)))

        # select 2nd from the bottom (NB python indexing is 0-based)
        vals = c.faces(">Z[1]").vals()
        self.assertEqual(len(vals), 2)

        val = c.faces(">Z[1]").val()
        self.assertAlmostEqual(val.Center().z, 1)

        # do the same but by selecting 3rd from the top
        vals = c.faces("<Z[2]").vals()
        self.assertEqual(len(vals), 2)

        val = c.faces("<Z[2]").val()
        self.assertAlmostEqual(val.Center().z, 1)

        # do the same but by selecting 2nd last from the bottom
        vals = c.faces("<Z[-2]").vals()
        self.assertEqual(len(vals), 2)

        val = c.faces("<Z[-2]").val()
        self.assertAlmostEqual(val.Center().z, 1)

        # verify that <Z[-1] is equivalent to <Z
        val1 = c.faces("<Z[-1]").val()
        val2 = c.faces("<Z").val()
        self.assertTupleAlmostEquals(val1.Center().toTuple(),
                                     val2.Center().toTuple(), 3)

        # verify that >Z[-1] is equivalent to >Z
        val1 = c.faces(">Z[-1]").val()
        val2 = c.faces(">Z").val()
        self.assertTupleAlmostEquals(val1.Center().toTuple(),
                                     val2.Center().toTuple(), 3)
Ejemplo n.º 3
0
    def testNthDistance(self):
        c = Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1)

        # 2nd face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val()
        self.assertAlmostEqual(val.Center().x, -1.5)

        # 2nd face with inversed selection vector
        val = c.faces(selectors.DirectionNthSelector(Vector(-1, 0, 0),
                                                     1)).val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # 2nd last face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0),
                                                     -2)).val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # Last face
        val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0),
                                                     -1)).val()
        self.assertAlmostEqual(val.Center().x, 2.5)

        # check if the selected face if normal to the specified Vector
        self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length,
                               0.0)

        # repeat the test using string based selector

        # 2nd face
        val = c.faces(">(1,0,0)[1]").val()
        self.assertAlmostEqual(val.Center().x, -1.5)
        val = c.faces(">X[1]").val()
        self.assertAlmostEqual(val.Center().x, -1.5)

        # 2nd face with inversed selection vector
        val = c.faces(">(-1,0,0)[1]").val()
        self.assertAlmostEqual(val.Center().x, 1.5)
        val = c.faces("<X[1]").val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # 2nd last face
        val = c.faces(">X[-2]").val()
        self.assertAlmostEqual(val.Center().x, 1.5)

        # Last face
        val = c.faces(">X[-1]").val()
        self.assertAlmostEqual(val.Center().x, 2.5)

        # check if the selected face if normal to the specified Vector
        self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length,
                               0.0)

        # test selection of multiple faces with the same distance
        c = (Workplane("XY").box(
            1, 4, 1, centered=(False, True, False)).faces("<Z").box(
                2, 2, 2, centered=(True, True, False)).faces(">Z").box(
                    1, 1, 1, centered=(True, True, False)))

        # select 2nd from the bottom (NB python indexing is 0-based)
        vals = c.faces(">Z[1]").vals()
        self.assertEqual(len(vals), 2)

        val = c.faces(">Z[1]").val()
        self.assertAlmostEqual(val.Center().z, 1)

        # do the same but by selecting 3rd from the top
        vals = c.faces("<Z[2]").vals()
        self.assertEqual(len(vals), 2)

        val = c.faces("<Z[2]").val()
        self.assertAlmostEqual(val.Center().z, 1)

        # do the same but by selecting 2nd last from the bottom
        vals = c.faces("<Z[-2]").vals()
        self.assertEqual(len(vals), 2)

        val = c.faces("<Z[-2]").val()
        self.assertAlmostEqual(val.Center().z, 1)

        # note that .val() will return the workplane center if the objects list
        # is empty, so to make sure this test fails with a selector that
        # selects nothing, use .vals()[0]
        # verify that <Z[-1] is equivalent to <Z
        val1 = c.faces("<Z[-1]").vals()[0]
        val2 = c.faces("<Z").vals()[0]
        self.assertTupleAlmostEquals(val1.Center().toTuple(),
                                     val2.Center().toTuple(), 3)

        # verify that >Z[-1] is equivalent to >Z
        val1 = c.faces(">Z[-1]").vals()[0]
        val2 = c.faces(">Z").vals()[0]
        self.assertTupleAlmostEquals(val1.Center().toTuple(),
                                     val2.Center().toTuple(), 3)

        # DirectionNthSelector should not select faces that are not perpendicular
        twisted_boxes = (Workplane().box(
            1, 1, 1,
            centered=(True, True, False)).transformed(rotate=(45, 0, 0),
                                                      offset=(0, 0,
                                                              3)).box(1, 1, 1))
        self.assertTupleAlmostEquals(
            twisted_boxes.faces(">Z[-1]").val().Center().toTuple(), (0, 0, 1),
            3)
        # this should select a face on the upper/rotated cube, not the lower/unrotated cube
        self.assertGreater(
            twisted_boxes.faces("<(0, 1, 1)[-1]").val().Center().z, 1)
        # verify that >Z[-1] is equivalent to >Z
        self.assertTupleAlmostEquals(
            twisted_boxes.faces(">(0, 1, 1)[0]").vals()[0].Center().toTuple(),
            twisted_boxes.faces("<(0, 1, 1)[-1]").vals()[0].Center().toTuple(),
            3,
        )
Ejemplo n.º 4
0
def bracket(self,
            thickness,
            height,
            width,
            offset=0,
            angle=90,
            hole_count=0,
            hole_diameter=None,
            edge_fillet=None,
            edge_chamfer=None,
            corner_fillet=None,
            corner_chamfer=None):
    """
    A CadQuery plugin to create an angle bracket along an edge.

    Must be used on a workplane that (1) coincides with the face on which to build the bracket, 
    (2) has its origin at the center of the edge along which to build the bracket and (3) has its 
    x axis pointing along the edge along which to build the bracket and (4) has its y axis pointing 
    away from the center of the face on which to build the bracket.

    :param …: todo

    .. todo:: Support to create only one hole in the bracket. Currently this results in "division 
        by float zero".
    .. todo:: Change the edge filleting so that it is done before cutting the holes, and so that 
        the holes are only cut into the non-filleted space. Otherwise the OCCT will often refuse, 
        as the fillet would interfere with an existing hole.
    .. todo:: Allow to specify fillets as "0", which should be converted to "None" in the constructor.
    .. todo:: Reimplement hole_coordinates() using Workplane::rarray(), see 
        https://cadquery.readthedocs.io/en/latest/classreference.html#cadquery.Workplane.rarray
    .. todo:: Extend the hole_coordinates() mechanism to also be able to generated two-dimensional
        hole patterns. A way to specify this would be hole_count = (2,3), meaning 2×3 holes. This 
        also requires to introduce a parameter "hole_margins", because margins between holes and 
        edges can no longer be automatically calculates as for a single line of holes.
    .. todo:: Make it possible to pass in two different lengths for the chamfer. That will allow 
        to create a better support of the core below it, where needed.
    .. todo:: Implement behavior for the angle parameter.
    .. todo:: Implement behavior for the offset parameter.
    .. todo:: Fix that the automatic hole positioning algorithm in hole_coordinates() does not work 
        well when the bracket's footprint is approaching square shape, or higher than wide.
    .. todo: Let this plugin determine its workplane by itself from the edge and face provided as 
        the top and second from top stack elements when called. That is however difficult because 
        the workplane has to be rotated so that the y axis points away from the center of the face 
        on which the bracket is being built.
    """
    def hole_coordinates(width, height, hole_count):
        v_offset = height / 2
        h_offset = width / 2 if hole_count == 1 else v_offset
        h_spacing = 0 if hole_count == 1 else (width -
                                               2 * offset) / (hole_count - 1)
        points = []

        # Go row-wise through all points from bottom to top and collect their coordinates.
        # (Origin is assumed in the lower left of the part's back surface.)
        for column in range(hole_count):
            points.append((h_offset + column * h_spacing, v_offset))

        log.info("hole coordinates = %s", points)
        return points

    cq.Workplane.translate_last = translate_last
    cq.Workplane.fillet_if = fillet_if
    cq.Workplane.chamfer_if = chamfer_if
    cq.Workplane.show_local_axes = show_local_axes

    # todo: Raise an argument error if both edge_fillet and edge_chamfer is given.
    # todo: Raise an argument error if both corner_fillet and corner_chamfer is given.

    result = self.newObject(self.objects)

    # Debug helper. Can only be used when executing utilities.py in cq-editor. Must be disabled
    # when importing utilities.py, as it will otherwise cause "name 'show_object' is not defined".
    # result.show_local_axes()

    # Determine the CadQuery primitive "Plane" object wrapped by the Workplane object. See:
    # https://cadquery.readthedocs.io/en/latest/_modules/cadquery/cq.html#Workplane
    plane = result.plane

    # Calculate various local directions as Vector objects using global coordinates.
    #
    # We want to convert a direction from local to global coordinates, not a point. A
    # direction is not affected by coordinate system offsetting, so we have to undo that
    # offset by subtracting the converte origin.
    dir_min_x = plane.toWorldCoords((-1, 0, 0)) - plane.toWorldCoords(
        (0, 0, 0))
    dir_max_x = plane.toWorldCoords((1, 0, 0)) - plane.toWorldCoords((0, 0, 0))
    dir_min_y = plane.toWorldCoords((0, -1, 0)) - plane.toWorldCoords(
        (0, 0, 0))
    dir_max_y = plane.toWorldCoords((0, 1, 0)) - plane.toWorldCoords((0, 0, 0))
    dir_min_z = plane.toWorldCoords((0, 0, -1)) - plane.toWorldCoords(
        (0, 0, 0))
    dir_max_z = plane.toWorldCoords((0, 0, 1)) - plane.toWorldCoords((0, 0, 0))
    dir_min_xz = plane.toWorldCoords((-1, 0, -1)) - plane.toWorldCoords(
        (0, 0, 0))

    result = (
        result

        # Create the bracket's cuboid base shape.
        .union(
            cq.Workplane()
            .copyWorkplane(result)
            .center(0, -thickness / 2)
            .box(width, thickness, height)
            # Raise the created box (dir_max_z in local coordinates). Since translate() requires
            # global coordinates, we have to use converted ones.
            .translate_last(dir_max_z * (height / 2))
        )

        # Cut the hole pattern into the bracket.
        # It's much easier to transform the workplane rather than creating a new one. Because for
        # a new workplane, z and x are initially aligned with respect to global coordinates, so the
        # coordinate system would have to be rotated for our needs, which is complex. Here we modify
        # the workplane to originate in the local bottom left corner of the bracket base shape.
        .transformed(offset = (-width / 2, 0), rotate = (90,0,0))
        .pushPoints(hole_coordinates(width, height, hole_count))
        .circle(hole_diameter / 2)
        .cutThruAll()

        # Fillets and chamfers.
        # The difficulty here is that we can't use normal CadQuery string selectors, as these always
        # refer to global directions, while inside this method we can only identify the direction
        # towards the bracket in our local coordinates. So we have to use the underlying selector
        # classes, and also convert from our local coordinates to the expected global ones manually.

        # Add a fillet along the bracketed edge if desired.
        .faces(cqs.DirectionNthSelector(dir_max_y, -2))
        # As a bracket on the other side might be present, we have to filter the selected faces
        # further to exclude that.
        .faces(cqs.DirectionMinMaxSelector(dir_max_z))
        .edges(cqs.DirectionMinMaxSelector(dir_min_z))
        .fillet_if(edge_fillet is not None, edge_fillet)

        # Add a chamfer along the bracketed edge if desired.
        .faces(cqs.DirectionNthSelector(dir_max_y, -2))
        .edges(cqs.DirectionMinMaxSelector(dir_min_z))
        .chamfer_if(edge_chamfer is not None, edge_chamfer)

        # Treat the bracket corners with a fillet if desired.
        .faces(cqs.DirectionMinMaxSelector(dir_max_z))
        .edges( # String selector equivalent in local coords: "<X or >X"
            cqs.SumSelector(
                cqs.DirectionMinMaxSelector(dir_min_x),
                cqs.DirectionMinMaxSelector(dir_max_x)
            )
        )
        .fillet_if(corner_fillet is not None, corner_fillet)

        # Treat the bracket corners with a chamfer if desired.
        .faces(cqs.DirectionMinMaxSelector(dir_max_z))
        .edges( # String selector equivalent in local coords: "<X or >X"
            cqs.SumSelector(
                cqs.DirectionMinMaxSelector(dir_min_x),
                cqs.DirectionMinMaxSelector(dir_max_x)
            )
        )
        .chamfer_if(corner_chamfer is not None, corner_chamfer)
    )
    return result