def testSumSelector(self): c = CQ(makeUnitCube()) S = selectors.StringSyntaxSelector fl = c.faces(selectors.SumSelector(S(">Z"), S("<Z"))).vals() self.assertEqual(2, len(fl)) el = c.edges(selectors.SumSelector(S("|X"), S("|Y"))).vals() self.assertEqual(8, len(el)) # test the sum operator fl = c.faces(S(">Z") + S("<Z")).vals() self.assertEqual(2, len(fl)) el = c.edges(S("|X") + S("|Y")).vals() self.assertEqual(8, len(el))
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