Source code for cadfile.cadutils.cadquery

#A Parametric CAD System for the Web
#Copyright (c) 2010-2022 Parametric Products Intellectual Holdings LLC, All Rights Reserved.

#
# TODO: Features Waiting for Implementation:
# General:
#     Plugins-- how to hook into the CQ layer to run arbitrary code?
#     example: plugabble object factory-- could be used by cbore, csk, box, etc
#       --> these all iterate over points, conditionally combine, etc
#
# 2D:
#     polar array-- subdivide circle by angle to create a list of points
#     midpoint -- subdivide edges to create points at midpoints
#     2d fillet/arc? Or would it be tangent arc between lines?
#
# Selectors:
#     midpoint selector
#     intersection selector
#     nth closest furthest selector: such as >Z[2] means 2nd maximum along z
#     rotated coordinate systems ( so that selectors can be used in other directions
#
# 3D:
#     create cylinder
#     create torus
#     create wedge
#     create sphere
#     workplane revolve through angle to create solid
#     sheet metal folding?
#
# Transformations:
#     rotate creating copy
#     translate creating copy

#from FreeCAD import Part
#from FreeCAD import Base
import re,math

#my modules
import SVGexporter
from cad import *


#TODO: .circle().circle() requires making this more robust to try all combinations of wire orders!
def sortWiresByBuildOrder(wireList,plane,result=[]):
    """
        Tries to determine how wires should be combined into faces.
        Assume:
            The wires make up one or more faces, which could have 'holes'
            Outer wires are listed ahead of inner wires
            there are no wires inside wires inside wires ( IE, islands -- we can deal with that later on )
            none of the wires are construction wires
        Compute:
            one or more sets of wires, with the outer wire listed first, and inner ones
        Returns, list of lists.
    """
    outerWire = wireList.pop(0)

    remainingWires = list(wireList)
    childWires = []
    for w in wireList:
        if plane.isWireInside(outerWire,w):
            childWires.append(remainingWires.pop(0))
        else:
            #doesnt match, assume this wire is a new outer
            result.append([outerWire] + childWires)
            return sortWiresByBuildOrder(remainingWires,plane,result)

    result.append([outerWire] + childWires)
    return result



class Selector(object):
[docs] """ Filters a list of objects Filters must provide a single method that filters objects. """ def filter(self,objectList):
[docs] """ Filter the provided list :param objectList: list to filter :type objectList: list of FreeCAD primatives :return: filtered list The default implementation returns the original list unfiltered """ return objectList class NearestToPointSelector(Selector):
[docs] """ Selects object nearest the provided point. If the object is a vertex or point, the distance is used. For other kinds of shapes, the center of mass is used to to compute which is closest. Applicability: All Types of Shapes Example:: CQ(aCube).vertices(NearestToPointSelector((0,1,0)) returns the vertex of the unit cube closest to the point x=0,y=1,z=0 """ def __init__(self,pnt ): self.pnt = pnt def filter(self,objectList): def dist(tShape): return tShape.Center().sub(self.pnt).Length #if tShape.ShapeType == 'Vertex': # return tShape.Point.sub(toVector(self.pnt)).Length #else: # return tShape.CenterOfMass.sub(toVector(self.pnt)).Length return [ min(objectList,key=dist) ] class BaseDirSelector(Selector):
""" A selector that handles selection on the basis of a single direction vector """ def __init__(self,vector,tolerance=0.0001 ): self.direction = vector self.TOLERANCE = tolerance def test(self,vec): "Test a specified vector. Subclasses override to provide other implementations" return True def filter(self,objectList): """ There are lots of kinds of filters, but for planes they are always based on the normal of the plane, and for edges on the tangent vector along the edge """ r = [] for o in objectList: #no really good way to avoid a switch here, edges and faces are simply different! if type(o) == Face: # a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir normal = o.normalAt(None) if self.test(normal): r.append(o) elif type(o) == Edge and o.geomType() == 'LINE': #an edge is parallel to a direction if it is a line, and the line is parallel to the dir tangent = o.tangentAt(None) if self.test(tangent): r.append(o) return r class ParallelDirSelector(BaseDirSelector):
[docs] """ Selects objects parallel with the provided direction Applicability: Linear Edges Planar Faces Use the string syntax shortcut \|(X|Y|Z) if you want to select based on a cardinal direction. Example:: CQ(aCube).faces(ParallelDirSelector((0,0,1)) selects faces with a normals in the z direction, and is equivalent to:: CQ(aCube).faces("|Z") """ def test(self,vec): return self.direction.cross(vec).Length < self.TOLERANCE class DirectionSelector(BaseDirSelector):
[docs] """ Selects objects aligned with the provided direction Applicability: Linear Edges Planar Faces Use the string syntax shortcut +/-(X|Y|Z) if you want to select based on a cardinal direction. Example:: CQ(aCube).faces(DirectionSelector((0,0,1)) selects faces with a normals in the z direction, and is equivalent to:: CQ(aCube).faces("+Z") """ def test(self,vec): return abs(self.direction.getAngle(vec) < self.TOLERANCE) class PerpendicularDirSelector(BaseDirSelector):
[docs] """ Selects objects perpendicular with the provided direction Applicability: Linear Edges Planar Faces Use the string syntax shortcut #(X|Y|Z) if you want to select based on a cardinal direction. Example:: CQ(aCube).faces(PerpendicularDirSelector((0,0,1)) selects faces with a normals perpendicular to the z direction, and is equivalent to:: CQ(aCube).faces("#Z") """ def test(self,vec): angle = self.direction.getAngle(vec) r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE ) return not r class TypeSelector(Selector):
[docs] """ Selects objects of the prescribed topological type. Applicability: Faces: Plane,Cylinder,Sphere Edges: Line,Circle,Arc You can use the shortcut selector %(PLANE|SPHERE|CONE) for faces, and %(LINE|ARC|CIRCLE) for edges. For example this:: CQ(aCube).faces ( TypeSelector("PLANE") ) will select 6 faces, and is equivalent to:: CQ(aCube).faces( "%PLANE" ) """ def __init__(self,typeString): self.typeString = typeString.upper() def filter(self,objectList): r = [] for o in objectList: if o.geomType() == self.typeString: r.append(o) return r class DirectionMinMaxSelector(Selector):
[docs] """ Selects objects closest or farthest in the specified direction Used for faces, points, and edges Applicability: All object types. for a vertex, its point is used. for all other kinds of objects, the center of mass of the object is used. You can use the string shortcuts >(X|Y|Z) or <(X|Y|Z) if you want to select based on a cardinal direction. For example this:: CQ(aCube).faces ( DirectionMinMaxSelector((0,0,1),True ) Means to select the face having the center of mass farthest in the positive z direction, and is the same as: CQ(aCube).faces( ">Z" ) Future Enhancements: provide a nicer way to select in arbitrary directions. IE, a bit more code could allow '>(0,0,1)' to work. """ def __init__(self,vector,directionMax=True): self.vector = vector self.max = max self.directionMax = directionMax def filter(self,objectList): #then sort by distance from origin, along direction specified def distance(tShape): return tShape.Center().dot(self.vector) #if tShape.ShapeType == 'Vertex': # pnt = tShape.Point #else: # pnt = tShape.Center() #return pnt.dot(self.vector) if self.directionMax: return [ max(objectList,key=distance) ] else: return [ min(objectList,key=distance) ] class StringSyntaxSelector(Selector):
[docs] """ Filter lists objects using a simple string syntax. All of the filters available in the string syntax are also available ( usually with more functionality ) through the creation of full-fledged selector objects. see :py:class:`Selector` and its subclasses Filtering works differently depending on the type of object list being filtered. :param selectorString: A two-part selector string, [selector][axis] :return: objects that match the specified selector ***Modfiers*** are ``('|','+','-','<','>','%')`` :\|: parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects. :#: perpendicular to (same as :py:class:`PerpendicularDirSelector` ) :+: positive direction (same as :py:class:`DirectionSelector` ) :-: negative direction (same as :py:class:`DirectionSelector` ) :>: maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True) :<: minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False ) :%: curve/surface type (same as :py:class:`TypeSelector`) ***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ`` Selectors are a complex topic: see :ref:`selector_reference` for more information """ def __init__(self,selectorString): self.axes = { 'X': Vector(1,0,0), 'Y': Vector(0,1,0), 'Z': Vector(0,0,1), 'XY': Vector(1,1,0), 'YZ': Vector(0,1,1), 'XZ': Vector(1,0,1) } namedViews = { 'front': ('>','Z' ), 'back': ('<','Z'), 'left':('<', 'X'), 'right': ('>', 'X'), 'top': ('>','Y'), 'bottom': ('<','Y') } self.selectorString = selectorString r = re.compile("\s*([-\+<>\|\%#])*\s*(\w+)\s*",re.IGNORECASE) m = r.match(selectorString) if m != None: if namedViews.has_key(selectorString): (a,b) = namedViews[selectorString] self.mySelector = self._chooseSelector(a,b ) else: self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1]) else: raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ") def _chooseSelector(self,selType,selAxis): """Sets up the underlying filters accordingly""" if selType == "%": return TypeSelector(selAxis) #all other types need to select axis as a vector #get the axis vector first, will throw an except if an unknown axis is used try: vec = self.axes[selAxis] except KeyError: raise ValueError ("Axis value %s not allowed: must be one of %s" % (selAxis, str(self.axes))) if selType in (None, "+"): #use direction filter return DirectionSelector(vec) elif selType == '-': #just use the reverse of the direction vector return DirectionSelector(vec.multiply(-1.0)) elif selType == "|": return ParallelDirSelector(vec) elif selType == ">": return DirectionMinMaxSelector(vec,True) elif selType == "<": return DirectionMinMaxSelector(vec,False) elif selType == '#': return PerpendicularDirSelector(vec) else: raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ") def filter(self,objectList):
[docs] """ selects minimum, maximum, positive or negative values relative to a direction [+\|-\|<\|>\|] \<X\|Y\|Z> """ return self.mySelector.filter(objectList) class CQContext(object):
""" A shared context for modeling. All objects in the same CQ chain share a reference to this same object instance which allows for shared state when needed, """ def __init__(self): self.pendingWires = [] #a list of wires that have been created and need to be extruded self.pendingEdges = [] #a list of pending edges that have been created and need to be joined into wires self.firstPoint = None #a reference to the first point for a set of edges. used to determine how to behave when close() is called self.tolerance = 0.0001 #user specified tolerance class CQ(object):
[docs] """ Provides enhanced functionality for a wrapped CAD primitive. Examples include feature selection, feature creation, 2d drawing using work planes, and 3d opertations like fillets, shells, and splitting Future Enhancements: 2D: polar array-- subdivide circle by angle to create a list of points midpoint -- subdivide edges to create points at midpoints 2d fillet/arc? Or would it be tangent arc between lines? Selectors: midpoint selector intersection selector nth closest furthest selector: such as >Z[2] means 2nd maximum along z rotated coordinate systems ( so that selectors can be used in other directions 3D: create cylinder create torus create wedge create sphere revolve through angle to create solid sheet metal folding? Transformations: rotate creating copy translate creating copy """ def __init__(self,obj): """ Construct a new cadquery (CQ) object that wraps a CAD primitive. :param obj: Object to Wrap. :type obj: A CAD Primitive ( wire,vertex,face,solid,edge ) """ self.objects = [] self.ctx = CQContext() self.parent = None if obj: #guarded because sometimes None for internal use self.objects.append(obj) def newObject(self,objlist):
[docs] """ Make a new CQ object. :param objlist: The stack of objects to use :param newContextSolid: an optional new solid to become the new context solid :type objlist: a list of CAD primitives ( wire,face,edge,solid,vertex,etc ) The parent of the new object will be set to the current object, to preserve the chain correctly. Custom plugins and subclasses should use this method to create new CQ objects correctly. """ r = CQ(None) #create a completely blank one r.parent = self r.ctx = self.ctx #context solid remains the same r.objects = list(objlist) return r def _collectProperty(self,propName):
""" Collects all of the values for propName, for all items on the stack. FreeCAD objects do not implement id correclty, so hashCode is used to ensure we dont add the same object multiple times. One weird use case is that the stack could have a solid reference object on it. This is meant to be a reference to the most recently modified version of the context solid, whatever it is. """ all = {} for o in self.objects: if hasattr(o,propName): for i in getattr(o,propName)(): all[i.hashCode()] = i return list(all.values()) def split(self,keepTop=False,keepBottom=False):
[docs] """ Splits a solid on the stack into two parts, optionally keeping the separate parts. :param boolean keepTop: True to keep the top, False or None to discard it :param boolean keepBottom: True to keep the bottom, False or None to discard it :raises: ValueError if keepTop and keepBottom are both false. :raises: ValueError if there is not a solid in the current stack or the parent chain :returns: CQ object with the desired objects on the stack. The most common operation splits a solid and keeps one half. This sample creates split bushing:: #drill a hole in the side c = Workplane().box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()F #now cut it in half sideways c.faces(">Y").workplane(-0.5).split(keepTop=True) """ solid = self.findSolid() if (not keepTop) and (not keepBottom): raise ValueError("You have to keep at least one half") maxDim = solid.BoundingBox().DiagonalLength * 10.0 topCutBox = self.rect(maxDim,maxDim)._extrude(maxDim) bottomCutBox = self.rect(maxDim,maxDim)._extrude(-maxDim) top = solid.cut(bottomCutBox) bottom = solid.cut(topCutBox) if keepTop and keepBottom: #put both on the stack, leave original unchanged return self.newObject([top,bottom]) else: # put the one we are keeping on the stack, and also update the context solid #to the one we kept if keepTop: solid.wrapped = top.wrapped return self.newObject([top]) else: solid.wrapped = bottom.wrapped return self.newObject([bottom]) def combineSolids(self,otherCQToCombine=None):
[docs] """ Combines all solids on the current stack, and any context object, together into a single object. After the operation, the returned solid is also the context solid. :param otherCQToCombine: another cadquery to combine. :return: a cQ object with the resulting combined solid on the stack. Most of the time, both objects will contain a single solid, which is combined and returned on the stack of the new object. """ #loop through current stack objects, and combine them #TODO: combine other types of objects as well, like edges and wires toCombine = self.solids().vals() if otherCQToCombine: for obj in otherCQToCombine.solids().vals(): toCombine.append(obj) if len(toCombine) < 1: raise ValueError("Cannot Combine: at least one solid required!") #get context solid ctxSolid = self.findSolid(searchStack=False,searchParents=True) #we dont want to find our own objects if ctxSolid is None: ctxSolid = toCombine.pop(0) #now combine them all. make sure to save a reference to the ctxSolid pointer! s = ctxSolid for tc in toCombine: s = s.fuse(tc) ctxSolid.wrapped = s.wrapped return self.newObject([s]) def all(self):
[docs] """ Return a list of all CQ objects on the stack. useful when you need to operate on the elements individually. Contrast with vals, which returns the underlying objects for all of the items on the stack """ return [self.newObject([o]) for o in self.objects] def size(self):
[docs] """ Return the number of objects currently on the stack """ return len(self.objects) def vals(self):
[docs] """ get the values in the current list :rtype: list of FreeCAD objects :returns: the values of the objects on the stack. Contrast with :py:meth:`all`, which returns CQ objects for all of the items on the stack """ res = [] return self.objects def add(self,obj):
[docs] """ adds an object or a list of objects to the stack :param obj: an object to add :type obj: a CQ object, CAD primitive, or list of CAD primitives :return: a CQ object with the requested operation performed If an CQ object, the values of that object's stack are added. If a list of cad primitives, they are all added. If a single CAD primitive it is added Used in rare cases when you need to combine the results of several CQ results into a single CQ object. Shelling is one common example """ if type(obj) == list: self.objects.extend(obj) elif type(obj) == CQ or type(obj) == Workplane: self.objects.extend(obj.objects) else: self.objects.append(obj) return self def val(self):
[docs] """ Return the first value on the stack :return: the first value on the stack. :rtype: A FreeCAD object or a SolidReference """ return self.objects[0] def workplane(self,offset=0.0,invert=False):
[docs] """ Creates a new 2-D workplane, located relative to the first face on the stack. :param offset: offset for the work plane in the Z direction. Default :param invert: invert the Z direction from that of the face. :type offset: float or None=0.0 :type invert: boolean or None=False :rtype: Workplane object ( which is a subclass of CQ ) The first element on the stack must be a face, or a vertex. If a vertex, then the parent item on the chain immediately before the vertex must be a face. The result will be a 2-d working plane with a new coordinate system set up as follows: * The origin will be located in the *center* of the face, if a face was selected. If a vertex was selected, the origin will be at the vertex, and located on the face. * The Z direction will be normal to the plane of the face,computed at the center point. * The X direction will be parallel to the x-y plane. If the workplane is parallel to the global x-y plane, the x direction of the workplane will co-incide with the global x direction. Most commonly, the selected face will be planar, and the workplane lies in the same plane of the face ( IE, offset=0). Occasionally, it is useful to define a face offset from an existing surface, and even more rarely to define a workplane based on a face that is not planar. To create a workplane without first having a face, use the Workplane() method. Future Enhancements: * Allow creating workplane from planar wires * Allow creating workplane based on an arbitrary point on a face, not just the center. For now you can work around by creating a workplane and then offsetting the center afterwards. """ obj = self.objects[0] def _computeXdir(normal): xd = Vector(0,0,1).cross(normal) if xd.Length < self.ctx.tolerance: #this face is parallel with the x-y plane, so choose x to be in global coordinates xd = Vector(1,0,0) return xd faceToBuildOn = None center = None #if isinstance(obj,Vertex): # f = self.parent.objects[0] # if f != None and isinstance(f,Face): # center = obj.Center() # normal = f.normalAt(center) # xDir = _computeXdir(normal) # else: # raise ValueError("If a vertex is selected, a face must be the immediate parent") if isinstance(obj,Face): faceToBuildOn = obj center = obj.Center() normal = obj.normalAt(center) xDir = _computeXdir(normal) else: if hasattr(obj,'Center'): center = obj.Center() normal = self.plane.zDir xDir = self.plane.xDir else: raise ValueError ("Needs a face or a vertex or point on a work plane") #invert if requested if invert: normal = normal.multiply(-1.0) #offset origin if desired offsetVector = normal.normalize().multiply(offset) offsetCenter = center.add(offsetVector) #make the new workplane plane = Plane(offsetCenter, xDir, normal) s = Workplane(plane) s.parent = self s.ctx = self.ctx #a new workplane has the center of the workplane on the stack return s def first(self):
[docs] """ Return the first item on the stack :returns: the first item on the stack. :rtype: a CQ object """ return self.newObject(self.objects[0:1]) def item(self,i):
[docs] """ Return the ith item on the stack. :rtype: a CQ object """ return self.newObject([self.objects[i]]) def last(self):
[docs] """ Return the last item on the stack. :rtype: a CQ object """ return self.newObject([self.objects[-1]]) def end(self):
[docs] """ Return the parent of this CQ element :rtype: a CQ object :raises: ValueError if there are no more parents in the chain. For example:: CQ(obj).faces("+Z").vertices().end() will return the same as:: CQ(obj).faces("+Z") """ if self.parent: return self.parent else: raise ValueError("Cannot End the chain-- no parents!") def findSolid(self,searchStack=True,searchParents=True):
[docs] """ Finds the first solid object in the chain, searching from the current node backwards through parents until one is found. :param searchStack: should objects on the stack be searched first. :param searchParents: should parents be searched? :raises: ValueError if no solid is found in the current object or its parents, and errorOnEmpty is True This function is very important for chains that are modifying a single parent object, most often a solid. Most of the time, a chain defines or selects a solid, and then modifies it using workplanes or other operations. Plugin Developers should make use of this method to find the solid that should be modified, if the plugin implements a unary operation, or if the operation will automatically merge its results with an object already on the stack. """ #notfound = ValueError("Cannot find a Valid Solid to Operate on!") if searchStack: for s in self.objects: if type(s) == Solid: return s if searchParents and self.parent is not None: return self.parent.findSolid(searchStack=True,searchParents=searchParents) return None def _selectObjects(self,objType,selector=None):
""" Filters objects of the selected type with the specified selector,and returns results :param objType: the type of object we are searching for :type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid) :return: a CQ object with the selected objects on the stack. **Implementation Note**: This is the base implmentation of the vertices,edges,faces,solids,shells, and other similar selector methods. It is a useful extension point for plugin developers to make other selector methods. """ toReturn = self._collectProperty(objType) #all of the faces from all objects on the stack, in a single list if selector is not None: if type(selector) == str: selectorObj = StringSyntaxSelector(selector) else: selectorObj = selector toReturn = selectorObj.filter(toReturn) return self.newObject(toReturn) def vertices(self,selector=None):
[docs] """ Select the vertices of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the vertices of all objects are collected and a list of all the distinct vertices is returned. :param selector: :type selector: None, a Selector object, or a string selector expression. :return: a CQ object whos stack contains the *distinct* vertices of *all* objects on the current stack, after being filtered by the selector, if provided If there are no vertices for any objects on the current stack, an empty CQ object is returned The typical use is to select the vertices of a single object on the stack. For example:: Workplane().box(1,1,1).faces("+Z").vertices().size() returns 4, because the topmost face of cube will contain four vertices. While this:: Workplane().box(1,1,1).faces().vertices().size() returns 8, because a cube has a total of 8 vertices **Note** Circles are peculiar, they have a single vertex at the center! :py:class:`StringSyntaxSelector` """ return self._selectObjects('Vertices',selector) def faces(self,selector=None):
[docs] """ Select the faces of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the faces of all objects are collected and a list of all the distinct faces is returned. :param selector: A selector :type selector: None, a Selector object, or a string selector expression. :return: a CQ object whos stack contains all of the *distinct* faces of *all* objects on the current stack, filtered by the provided selector. If there are no vertices for any objects on the current stack, an empty CQ object is returned The typical use is to select the faces of a single object on the stack. For example:: CQ(aCube).faces("+Z").size() returns 1, because a cube has one face with a normal in the +Z direction. Similarly:: CQ(aCube).faces().size() returns 6, because a cube has a total of 6 faces, And:: CQ(aCube).faces("|Z").size() returns 2, because a cube has 2 faces having normals parallel to the z direction See more about selectors HERE """ return self._selectObjects('Faces',selector) def edges(self,selector=None):
[docs] """ Select the edges of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the edges of all objects are collected and a list of all the distinct edges is returned. :param selector: A selector :type selector: None, a Selector object, or a string selector expression. :return: a CQ object whos stack contains all of the *distinct* edges of *all* objects on the current stack, filtered by the provided selector. If there are no edges for any objects on the current stack, an empty CQ object is returned The typical use is to select the edges of a single object on the stack. For example:: CQ(aCube).faces("+Z").edges().size() returns 4, because a cube has one face with a normal in the +Z direction. Similarly:: CQ(aCube).edges().size() returns 12, because a cube has a total of 12 edges, And:: CQ(aCube).edges("|Z").size() returns 4, because a cube has 4 edges parallel to the z direction See more about selectors HERE """ return self._selectObjects('Edges',selector) def wires(self,selector=None):
[docs] """ Select the wires of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the wires of all objects are collected and a list of all the distinct wires is returned. :param selector: A selector :type selector: None, a Selector object, or a string selector expression. :return: a CQ object whos stack contains all of the *distinct* wires of *all* objects on the current stack, filtered by the provided selector. If there are no wires for any objects on the current stack, an empty CQ object is returned The typical use is to select the wires of a single object on the stack. For example:: CQ(aCube).faces("+Z").wires().size() returns 1, because a face typically only has one outer wire See more about selectors HERE """ return self._selectObjects('Wires',selector) def solids(self,selector=None):
[docs] """ Select the solids of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the solids of all objects are collected and a list of all the distinct solids is returned. :param selector: A selector :type selector: None, a Selector object, or a string selector expression. :return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. If there are no solids for any objects on the current stack, an empty CQ object is returned The typical use is to select the a single object on the stack. For example:: CQ(aCube).solids().size() returns 1, because a cube consists of one solid. It is possible for single CQ object ( or even a single CAD primitive ) to contain multiple solids. See more about selectors HERE """ return self._selectObjects('Solids',selector) def shells(self,selector=None):
[docs] """ Select the shells of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the shells of all objects are collected and a list of all the distinct shells is returned. :param selector: A selector :type selector: None, a Selector object, or a string selector expression. :return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. If there are no shells for any objects on the current stack, an empty CQ object is returned Most solids will have a single shell, which represents the outer surface. A shell will typically be composed of multiple faces. See more about selectors HERE """ return self._selectObjects('Shells',selector) def compounds(self,selector=None):
[docs] """ Select compounds on the stack, optionally filtering the selection. If there are multiple objects on the stack, they are collected and a list of all the distinct compounds is returned. :param selector: A selector :type selector: None, a Selector object, or a string selector expression. :return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. A compound contains multiple CAD primitives that resulted from a single operation, such as a union, cut, split, or fillet. Compounds can contain multiple edges, wires, or solids. See more about selectors HERE """ return self._selectObjects('Compounds',selector) def toSvg(self,opts=None):
[docs] """ Returns svg text that represents the first item on the stack. for testing purposes. :param options: svg formatting options :type options: dictionary, width and height :return: a string that contains SVG that represents this item. """ return SVGexporter.getSVG(self.val().wrapped,opts) def exportSvg(self,fileName):
[docs] """ Exports the first item on the stack as an SVG file For testing purposes mainly. :param fileName: the filename to export :type fileName: String, absolute path to the file """ SVGexporter.exportSVG(self.val().wrapped,fileName) def rotateAboutCenter(self,axisEndPoint,angleDegrees):
[docs] """ Rotates all items on the stack by the specified angle, about the specified axis The center of rotation is a vector starting at the center of the object on the stack, and ended at the specified point. :param axisEndPoint: the second point of axis of rotation :type axisEndPoint: a three-tuple in global coordinates :param angleDegrees: the rotation angle, in degrees :type angleDegrees: float :returns: a CQ object, with all items rotated. WARNING: This version returns the same cq object instead of a new one-- the old object is not accessible. Future Enhancements: * A version of this method that returns a transformed copy, rather than modifying the originals * This method doesnt expose a very good interface, becaues the axis of rotation could be inconsistent between multiple objects. This is because the beginning of the axis is variable, while the end is fixed. This is fine when operating on one object, but is not cool for multiple. """ #center point is the first point in the vector endVec = Vector(axisEndPoint) def _rot(obj): startPt = obj.Center() endPt = startPt + endVec obj.rotate(startPt,endPt,angleDegrees) return self.each(_rot,False) def translate(self,vec):
[docs] """ Returns a copy of all of the items on the stack by the specified distance :param tupleDistance: distance to move, in global coordinates :type tupleDistance: a 3-tuple of float :returns: a CQ object WARNING: the underlying objects are modified, not copied. Future Enhancements: A version of this method that returns a transformed copy instead of modifying the originals. """ return self.newObject([o.translate(vec) for o in self.objects]) def shell(self,thickness):
[docs] """ Remove the selected faces to create a shell of the specified thickness. To shell, first create a solid, and *in the same chain* select the faces you wish to remove. :param thickness: a positive float, representing the thickness of the desired shell. Negative values shell inwards, positive values shell outwards. :raises: ValueError if the current stack contains objects that are not faces of a solid further up in the chain. :returns: a CQ object with the resulting shelled solid selected. This example will create a hollowed out unit cube, where the top most face is open, and all other walls are 0.2 units thick:: Workplane().box(1,1,1).faces("+Z").shell(0.2) Shelling is one of the cases where you may need to use the add method to select several faces. For example, this example creates a 3-walled corner, by removing three faces of a cube:: s = Workplane().box(1,1,1) s1 = s.faces("+Z") s1.add(s.faces("+Y")).add(s.faces("+X")) self.saveModel(s1.shell(0.2)) This fairly yucky syntax for selecting multiple faces is planned for improvement **Note**: When sharp edges are shelled inwards, they remain sharp corners, but **outward** shells are automatically filleted, because an outward offset from a corner generates a radius Future Enhancements: Better selectors to make it easier to select multiple faces """ solidRef = self.findSolid() for f in self.objects: if type(f) != Face: raise ValueError ("Shelling requires that faces be selected") s = solidRef.shell(self.objects,thickness) solidRef.wrapped = s.wrapped return self.newObject([s]) def fillet(self,radius):
[docs] """ Fillets a solid on the selected edges. The edges on the stack are filleted. The solid to which the edges belong must be in the parent chain of the selected edges. :param radius: the radius of the fillet, must be > zero :type radius: positive float :raises: ValueError if at least one edge is not selected :raises: ValueError if the solid containing the edge is not in the chain :returns: cq object with the resulting solid selected. This example will create a unit cube, with the top edges filleted:: s = Workplane().box(1,1,1).faces("+Z").edges().fillet(0.1) """ #TODO: we will need much better edge selectors for this to work #TODO: ensure that edges selected actually belong to the solid in the chain, otherwise, fe segfault solid = self.findSolid() edgeList = self.edges().vals() if len(edgeList) < 1: raise ValueError ("Fillets requires that edges be selected") s = solid.fillet(radius,edgeList) solid.wrapped = s.wrapped return self.newObject([s]) class Workplane(CQ):
[docs] """ Defines a coordinate system in space, in which 2-d coordinates can be used. :param plane: the plane in which the workplane will be done :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right) :param origin: the desired origin of the new workplane :type origin: a 3-tuple in global coordinates, or None to default to the origin :param obj: an object to use initially for the stack :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value. :raises: ValueError if the provided plane is not a plane, a valid named workplane :return: A Workplane object, with coordinate system matching the supplied plane. The most common use is:: s = Workplane("XY") After creation, the stack contains a single point, the origin of the underlying plane, and the *current point* is on the origin. .. note:: You can also create workplanes on the surface of existing faces using :py:meth:`CQ.workplane` """ FOR_CONSTRUCTION = 'ForConstruction' def __init__(self, inPlane ,origin=(0,0,0), obj=None): """ make a workplane from a particular plane :param plane: the plane in which the workplane will be done :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right) :param origin: the desired origin of the new workplane :type origin: a 3-tuple in global coordinates, or None to default to the origin :param obj: an object to use initially for the stack :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value. :raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ :return: A Workplane object, with coordinate system matching the supplied plane. The most common use is:: s = Workplane("XY") After creation, the stack contains a single point, the origin of the underlying plane, and the *current point* is on the origin. """ if inPlane.__class__.__name__ == 'Plane': tmpPlane = inPlane elif type(inPlane) == str: tmpPlane = Plane.named(inPlane,origin) else: tmpPlane = None if tmpPlane == None: raise ValueError(" Provided value %s is not a valid work plane." % str(inPlane)) self.obj = obj self.plane = tmpPlane self.firstPoint = None self.objects = [self.plane.origin] #changed so that workplane has the center as the first item on the stack self.parent = None self.ctx = CQContext() def transformed(self,rotate=Vector(0,0,0),offset=Vector(0,0,0)):
[docs] """ Create a new workplane based on the current one. The origin of the new plane is located at the existing origin+offset vector, where offset is given in coordinates local to the current plane The new plane is rotated through the angles specified by the components of the rotation vector :param rotate: vector of angles to rotate, in degrees relative to work plane coordinates :param offset: vector to offset the new plane, in local work plane coordinates :return: a new work plane, transformed as requested """ p = self.plane.rotated(rotate) p.setOrigin3d(self.plane.toWorldCoords(offset.toTuple() )) ns = self.newObject([p.origin]) ns.plane = p return ns def newObject(self,objlist):
[docs] """ Create a new workplane object from this one. Overrides CQ.newObject, and should be used by extensions, plugins, and subclasses to create new objects. :param objlist: new objects to put on the stack :type objlist: a list of CAD primitives :return: a new Workplane object with the current workplane as a parent. """ #copy the current state to the new object ns = Workplane("XY") ns.plane = self.plane ns.parent = self ns.objects = list(objlist) ns.ctx = self.ctx return ns def _findFromPoint(self,useLocalCoords=False):
""" finds the start point for an operation when an existing point is implied. Examples include 2d operations such as lineTo, which allows specifying the end point, and implicitly use the end of the previous line as the starting point :return: a Vector representing the point to use, or none if such a point is not available. :param useLocalCoords: selects whether the point is returned in local coordinates or global coordinates. The algorithm is this: * If an Edge is on the stack, its end point is used.yp * if a vector is on the stack, it is used WARNING: only the first object on the stack is used. NOTE: """ obj = self.objects[0] p = None if isinstance(obj,Edge): p = obj.endPoint() elif isinstance(obj,Vector): p = obj else: raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) if useLocalCoords: return self.plane.toLocalCoords(p) else: return p def pushPoints(self,pntList):
[docs] """ Pushes a list of points onto the stack as vertices. The points are in the 2-d coordinate space of the workplane face :param pntList: a list of points to push onto the stack :type pntList: list of 2-tuples, in *local* coordinates :return: a new workplane with the desired points on the stack. A common use is to provide a list of points for a subsequent operation, such as creating circles or holes. This example creates a cube, and then drills three holes through it, based on three points:: s = Workplane().box(1,1,1).faces(">Z").workplane().pushPoints([(-0.3,0.3),(0.3,0.3),(0,0)]) body = s.circle(0.05).cutThruAll() Here the circle function operates on all three points, and is then extruded to create three holes. See :py:meth:`circle` for how it works. """ vecs = [] for pnt in pntList: vec = self.plane.toWorldCoords(pnt) vecs.append(vec) return self.newObject(vecs) def center(self,x,y):
[docs] """ Shift local coordinates to the specified location. The location is specified in terms of local coordinates. :param float x: the new x location :param float y: the new y location :returns: the workplane object, with the center adjusted. The current point is set to the new center. This method is useful to adjust the center point after it has been created automatically on a face, but not where you'd like it to be. In this example, we adjust the workplane center to be at the corner of a cube, instead of the center of a face, which is the default:: #this workplane is centered at x=0.5,y=0.5, the center of the upper face s = Workplane().box(1,1,1).faces(">Z").workplane() s.center(-0.5,-0.5) # move the center to the corner t = s.circle(0.25).extrude(0.2) assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner The result is a cube with a round boss on the corner """ "Shift local coordinates to the specified location, according to current coordinates" self.plane.setOrigin2d(x,y) n = self.newObject([self.plane.origin]) return n def lineTo(self, x, y,forConstruction=False):
[docs] """ Make a line from the current point to the provided point :param float x: the x point, in workplane plane coordinates :param float y: the y point, in workplane plane coordinates :return: the Workplane object with the current point at the end of the new line see :py:meth:`line` if you want to use relative dimensions to make a line instead. """ startPoint = self._findFromPoint(False) endPoint = self.plane.toWorldCoords((x, y)) p = Edge.makeLine(startPoint,endPoint) if not forConstruction: self._addPendingEdge(p) return self.newObject([p]) #line a specified incremental amount from current point def line(self, xDist, yDist ,forConstruction=False):
[docs] """ Make a line from the current point to the provided point, using dimensions relative to the current point :param float xDist: x distance from current point :param float yDist: y distance from current point :return: the workplane object with the current point at the end of the new line see :py:meth:`lineTo` if you want to use absolute coordinates to make a line instead. """ p = self._findFromPoint(True) #return local coordinates return self.lineTo(p.x + xDist, yDist + p.y,forConstruction) def vLine(self, distance,forConstruction=False):
[docs] """ Make a vertical line from the current point the provided distance :param float distance: (y) distance from current point :return: the workplane object with the current point at the end of the new line """ return self.line(0, distance,forConstruction) def vLineTo(self,yCoord,forConstruction=False):
[docs] """ Make a vertcial line from the current point to the provided y coordinate. Useful if it is more convienient to specify the end location rather than distance, as in :py:meth:`vLine` :param float yCoord: y coordinate for the end of the line :return: the Workplane object with the current point at the end of the new line """ p = self._findFromPoint(True) return self.lineTo(p.x,yCoord,forConstruction) def hLineTo(self,xCoord,forConstruction=False):
[docs] """ Make a horizontal line from the curren tpoint to the provided x coordinate. Useful if it is more convienient to specify the end location rather than distance, as in :py:meth:`hLine` :param float xCoord: x coordinate for the end of the line :return: the Workplane object with the current point at the end of the new line """ p = self._findFromPoint(True) return self.lineTo(xCoord,p.y,forConstruction) def hLine(self, distance,forConstruction=False):
[docs] """ Make a horizontal line from the current point the provided distance :param float distance: (x) distance from current point :return: the Workplane object with the current point at the end of the new line """ return self.line(distance, 0,forConstruction) #absolute move in current plane, not drawing def moveTo(self, x=0, y=0):
[docs] """ Move to the specified point, without drawing. :param x: desired x location, in local coordinates :type x: float, or none for zero :param y: desired y location, in local coorindates :type y: float, or none for zero. Not to be confused with :py:meth:`center`, which moves the center of the entire workplane, this method only moves the current point ( and therefore does not affect objects already drawn ). See :py:meth:`move` to do the same thing but using relative dimensions """ newCenter = Vector(x,y,0) return self.newObject([self.plane.toWorldCoordinates(newCenter)]) #relative move in current plane, not drawing def move(self, xDist=0, yDist=0):
[docs] """ Move the specified distance from the current point, without drawing. :param xDist: desired x distance, in local coordinates :type xDist: float, or none for zero :param yDist: desired y distance, in local coorindates :type yDist: float, or none for zero. Not to be confused with :py:meth:`center`, which moves the center of the entire workplane, this method only moves the current point ( and therefore does not affect objects already drawn ). See :py:meth:`moveTo` to do the same thing but using absolute coordinates """ p = self._findFromPoint(True) newCenter = p + Vector(xDist,yDist,0) return self.newObject([self.plane.toWorldCoordinates(newCenter)]) def spline(self,listOfXYTuple,forConstruction=False):
[docs] """ Create a spline interpolated through the provided points. :param listOfXYTuple: points to interpolate through :type listOfXYTuple: list of 2-tuple :return: a Workplane object with the current point at the end of the spline The spline will begin at the current point, and end with the last point in the XY typle list This example creates a block with a spline for one side:: s = Workplane(Plane.XY()) sPnts = [ (2.75,1.5), (2.5,1.75), (2.0,1.5), (1.5,1.0), (1.0,1.25), (0.5,1.0), (0,1.0) ] r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close() r = r.extrude(0.5) *WARNING* It is fairly easy to create a list of points that cannot be correctly interpreted as a spline. Future Enhancements: * provide access to control points """ gstartPoint = self._findFromPoint(False) gEndPoint = self.plane.toWorldCoords(listOfXYTuple[-1]) vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ] allPoints = [gstartPoint] + vecs e = Edge.makeSpline(allPoints) if not forConstruction: self._addPendingEdge(e) return self.newObject([e]) def threePointArc(self,point1, point2,forConstruction=False):
[docs] """ Draw an arc from the current point, through point1, and ending at point2 :param point1: point to draw through :type point1: 2-tuple, in workplane coordinates :param point2: end point for the arc :type point2: 2-tuple, in workplane coordinates :return: a workplane with the current point at the end of the arc Future Enhancments: provide a version that allows an arc using relative measures provide a centerpoint arc provide tangent arcs """ gstartPoint = self._findFromPoint(False) gpoint1 = self.plane.toWorldCoords(point1) gpoint2 = self.plane.toWorldCoords(point2) arc = Edge.makeThreePointArc(gstartPoint,gpoint1,gpoint2) if not forConstruction: self._addPendingEdge(arc) return self.newObject([arc]) def rotateAndCopy(self,matrix):
[docs] """ Makes a copy of all edges on the stack, rotates them according to the provided matrix, and then attempts to consolidate them into a single wire. :param matrix: a 4xr transformation matrix, in global coordinates :type matrix: a FreeCAD Base.Matrix object :return: a cadquery object with consolidated wires, and any originals on the stack. The most common use case is to create a set of open edges, and then mirror them around either the X or Y axis to complete a closed shape. see :py:meth:`mirrorX` and :py:meth:`mirrorY` to mirror about the global X and Y axes see :py:meth:`mirrorX` and for an example Future Enhancements: faster implementation: this one transforms 3 times to accomplish the result """ #compute rotation matrix ( global --> local --> rotate --> global ) rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG) #convert edges to a wire, if there are pending edges n = self.wire(forConstruction=False) #attempt to consolidate wires together. consolidated = n.consolidateWires() #ok, mirror all the wires. #There might be a better way, but to do this rotation takes 3 steps #transform geometry to local coordinates #then rotate about x #then transform back to global coordiante originalWires = consolidated.wires().vals() for w in originalWires: mirrored = w.transformGeometry(rm) consolidated.objects.append(mirrored) consolidated._addPendingWire(mirrored) #attempt again to consolidate all of the wires c = consolidated.consolidateWires() #c = consolidated return c def mirrorY(self):
[docs] """ Mirror entities around the y axis of the workplane plane. :return: a new object with any free edges consolidated into as few wires as possible. All free edges are collected into a wire, and then the wire is mirrored, and finally joined into a new wire Typically used to make creating wires with symmetry easier. This line of code:: s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25) Produces a flat, heart shaped object Future Enhancements: mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness """ tm = Base.Matrix() tm.rotateY(math.pi) return self.rotateAndCopy(tm) def mirrorX(self):
[docs] """ Mirror entities around the x axis of the workplane plane. :return: a new object with any free edges consolidated into as few wires as possible. All free edges are collected into a wire, and then the wire is mirrored, and finally joined into a new wire Typically used to make creating wires with symmetry easier. Future Enhancements: mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness """ tm = Base.Matrix() tm.rotateX(math.pi) return self.rotateAndCopy(tm) def _addPendingEdge(self,edge):
""" Queues an edge for later combination into a wire. :param edge: :return: """ self.ctx.pendingEdges.append(edge) if self.ctx.firstPoint is None: self.ctx.firstPoint = edge.startPoint() def _addPendingWire(self,wire): """ Queue a Wire for later extrusion Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids. but users do not normally care about these distinctions. Users 'think' in terms of edges, and solids. CadQuery tracks edges as they are drawn, and automatically combines them into wires when the user does an operation that needs it. Similarly, cadQuery tracks pending wires, and automaticlaly combines them into faces when necessary to make a solid. """ self.ctx.pendingWires.append(wire) def consolidateWires(self):
[docs] """ Attempt to consolidate wires on the stack into a single. If possible, a new object with the results are returned. if not possible, the wires remain separated FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly somtimes Additionally, it has a bug where a profile compose of two wires ( rathre than one ) also does not work properly together these are a real problem. """ wires = self.wires().vals() if len(wires) < 2: return self #TODO: this makes the assumption that either all wires could be combined, or none. #in reality trying each combination of wires is probably not reasonable anyway w = Wire.combine(wires) #ok this is a little tricky. if we consolidate wires, we have to actually #modify the pendingWires collection to remove the original ones, and replace them #with the consolidate done #since we are already assuming that all wires could be consolidated, its easy, we just #clear the pending wire list r = self.newObject([w]) r.ctx.pendingWires = [] r._addPendingWire(w) return r def wire(self,forConstruction=False):
[docs] """ Returns a CQ object with all pending edges connected into a wire. All edges on the stack that can be combined will be combined into a single wire object, and other objects will remain on the stack unmodified :param forConstruction: whether the wire should be used to make a solid, or if it is just for reference :type forConstruction: boolean. true if the object is only for reference This method is primarily of use to plugin developers making utilites for 2-d construction. This method shoudl be called when a user operation implies that 2-d construction is finished, and we are ready to begin working in 3d SEE '2-d construction concepts' for a more detailed explanation of how cadquery handles edges, wires, etc Any non edges will still remain. """ edges = self.ctx.pendingEdges #do not consolidate if there are no free edges if len(edges) == 0: return self self.ctx.pendingEdges = [] others = [] for e in self.objects: if type(e) != Edge: others.append(e) w = Wire.assembleEdges(edges) if not forConstruction: self._addPendingWire(w) return self.newObject(others + [w]) def each(self,callBackFunction,useLocalCoordinates=False):
[docs] """ runs the provided function on each value in the stack, and collects the return values into a new CQ object. Special note: a newly created workplane always has its center point as its only stack item :param callBackFunction: the function to call for each item on the current stack. :param useLocalCoordinates: should values be converted from local coordinates first? :type useLocalCoordinates: boolean The callback function must accept one argument, which is the item on the stack, and return one object, which is collected. If the function returns None, nothing is added to the stack. The object passed into the callBackFunction is potentially transformed to local coordinates, if useLocalCoordinates is true useLocalCoordinates is very useful for plugin developers. If false, the callback function is assumed to be working in global coordinates. Objects created are added as-is, and objects passed into the function are sent in using global coordinates If true, the calling function is assumed to be working in local coordinates. Objects are transformed to local coordinates before they are passed into the callback method, and result objects are transformed to global coorindates after they are returned. This allows plugin developers to create objects in local coordinates, without worrying about the fact that the working plane is different than the global coordinate system. TODO: wrapper object for Wire will clean up forConstruction flag everywhere """ results = [] for obj in self.objects: if useLocalCoordinates: #TODO: this needs to work for all types of objects, not just vectors! r = callBackFunction(self.plane.toLocalCoords(obj)) r = r.transformShape(self.plane.rG) else: r = callBackFunction(obj) if type(r) == Wire: if not r.forConstruction: self._addPendingWire(r) results.append ( r ) return self.newObject(results) def eachpoint(self,callbackFunction, useLocalCoordinates=False):
[docs] """ Same as each(), except each item on the stack is converted into a point before it is passed into the callback function. :return: CadQuery object which contains a list of vectors (points ) on its stack. :param useLocalCoordinates: should points be in local or global coordinates :type useLocalCoordinates: boolean The resulting object has a point on the stack for each object on the original stack. Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted to a point by using their center of mass. If the stack has zero length, a single point is returned, which is the center of the current workplane/coordinate system """ #convert stack to a list of points pnts = [] if len(self.objects) == 0: #nothing on the stack. here, we'll assume we should operate with the #origin as the context point pnts.append(self.plane.origin) else: for v in self.objects: pnts.append(v.Center()) return self.newObject(pnts).each(callbackFunction,useLocalCoordinates ) #make a rectangle def rect(self,xLen,yLen,centered=True,forConstruction=False):
[docs] """ Make a rectangle for each item on the stack. :param xLen: length in xDirection ( in workplane coordinates ) :type xLen: float > 0 :param yLen: length in yDirection ( in workplane coordinates ) :type yLen: float > 0 :param boolean centered: true if the rect is centered on the reference point, false if the lower-left is on the reference point :param forConstruction: should the new wires be reference geometry only? :type forConstruction: true if the wires are for reference, false if they are creating part geometry :return: a new CQ object with the created wires on the stack A common use case is to use a for-construction rectangle to define the centers of a hole pattern:: s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25) Creates 4 circles at the corners of a square centered on the origin. Future Enhancements: better way to handle forConstruction project points not in the workplane plane onto the workplane plane """ def makeRectangleWire(pnt): #here pnt is in local coordinates due to useLocalCoords=True (xc,yc,zc) = pnt.toTuple() if centered: p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0,0) ) p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0,0) ) p3 = pnt.add(Vector(xLen/2.0, yLen/2.0,0) ) p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0,0) ) else: p1 = pnt p2 = pnt.add(Vector(xLen,0,0)) p3 = pnt.add(Vector( xLen,yLen,0 )) p4 = pnt.add(Vector(0,yLen,0)) w = Wire.makePolygon([p1,p2,p3,p4,p1],forConstruction) return w #return Part.makePolygon([p1,p2,p3,p4,p1]) return self.eachpoint(makeRectangleWire,True) #circle from current point def circle(self,radius,forConstruction=False):
[docs] """ Make a circle for each item on the stack. :param radius: radius of the circle :type radius: float > 0 :param forConstruction: should the new wires be reference geometry only? :type forConstruction: true if the wires are for reference, false if they are creating part geometry :return: a new CQ object with the created wires on the stack A common use case is to use a for-construction rectangle to define the centers of a hole pattern:: s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25) Creates 4 circles at the corners of a square centered on the origin. Another common case is to use successive circle() calls to create concentric circles. This works because the center of a circle is its reference point:: s = Workplane().circle(2.0).circle(1.0) Creates two concentric circles, which when extruded will form a ring. Future Enhancements: better way to handle forConstruction project points not in the workplane plane onto the workplane plane """ def makeCircleWire(obj): cir = Wire.makeCircle(radius,obj,Vector(0,0,1)) cir.forConstruction = forConstruction return cir return self.eachpoint(makeCircleWire,useLocalCoordinates=True) def polygon(self,nSides,diameter):
[docs] """ Creates a polygon incribed in a circle of the specified diamter for each point on the stack The first vertex is always oriented in the x direction. :param nSides: number of sides, must be > 3 :param diameter: the size of the circle the polygon is incribed into :return: a polygon wire """ def _makePolygon(center): #pnt is a vector in local coordinates angle = 2.0 *math.pi / nSides pnts = [] for i in range(nSides+1): pnts.append( center + Vector((diameter / 2.0 * math.cos(angle*i)),(diameter / 2.0 * math.sin(angle*i)),0)) return Wire.makePolygon(pnts) return self.eachpoint(_makePolygon,True) def polyline(self,listOfXYTuple,forConstruction=False):
[docs] """ Create a polyline from a list of points :param listOfXYTuple: a list of points in Workplane coordinates :type listOfXYTuple: list of 2-tuples :param forConstruction: should the new wire be reference geometry only? :type forConstruction: true if the wire is for reference, false if they are creating part geometry :return: a new CQ object with the new wire on the stack *NOTE* most commonly, the resulting wire should be closed. Future Enhacement: This should probably yield a list of edges, not a wire, so that it is possible to combine a polyline with other edges and arcs """ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ] w = Wire.makePolygon(vecs) if not forConstruction: self._addPendingWire(w) return self.newObject([w]) #finish a set of lines. # def close(self):
[docs] """ End 2-d construction, and attempt to build a closed wire. :return: a CQ object with a completed wire on the stack, if possible. After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary to convert the edges produced by these into one or more wires. When a set of edges is closed, cadQuery assumes it is safe to build the group of edges into a wire. This example builds a simple triangular prism:: s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2) """ self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y) return self.wire() def largestDimension(self):
[docs] """ Finds the largest dimension in the stack. Used internally to create thru features, this is how you can compute how long or wide a feature must be to make sure to cut through all of the material :return: """ #TODO: this implementation is naive and returns the dims of the first solid... most of #the time this works. but a stronger implementation would be to search all solids. s = self.findSolid() if s: return s.BoundingBox().DiagonalLength * 5.0 else: return 1000000 def cutEach(self,fcn,useLocalCoords=False):
[docs] """ Evaluates the provided function at each point on the stack ( ie, eachpoint ) and then cuts the result from the context solid. :param function: a function suitable for use in the eachpoint method: ie, that accepts a vector :param useLocalCoords: same as for :py:meth:`eachpoint` :return: a CQ object that contains the resulting solid :raises: an error if there is not a context solid to cut from """ ctxSolid = self.findSolid() if ctxSolid is None: raise ValueError ("Must have a solid in the chain to cut from!") #will contain all of the counterbores as a single compound results = self.eachpoint(fcn,useLocalCoords).vals() s = ctxSolid for cb in results: s = s.cut(cb) ctxSolid.wrapped = s.wrapped return self.newObject([s]) #but parameter list is different so a simple function pointer wont work def cboreHole(self,diameter,cboreDiameter,cboreDepth,depth=None):
[docs] """ Makes a counterbored hole for each item on the stack. :param diameter: the diameter of the hole :type diamter: float > 0 :param cboreDiameter: the diameter of the cbore :type cboreDiameter: float > 0 and > diameter :param cboreDepth: depth of the counterbore :type cboreDepth: float > 0 :param depth: the depth of the hole :type depth: float > 0 or None to drill thru the entire part. The surface of the hole is at the current workplane plane. One hole is created for each item on the stack. A very common use case is to use a construction rectangle to define the centers of a set of holes, like so:: s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\ .vertices().cboreHole(0.125, 0.25,0.125,depth=None) This sample creates a plate with a set of holes at the corners. **Plugin Note**: this is one example of the power of plugins. Counterbored holes are quite time consuming to create, but are quite easily defined by users. see :py:meth:`cskHole` to make countersinks instead of counterbores """ if depth is None: depth = self.largestDimension() def _makeCbore(center): """ Makes a single hole with counterbore at the supplied point returns a solid suitable for subtraction pnt is in local coordinates """ boreDir = Vector(0,0,-1) #first make the hole hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates! #add the counter bore cbore = Solid.makeCylinder(cboreDiameter/2.0,cboreDepth,center,boreDir) r = hole.fuse(cbore) return r return self.cutEach(_makeCbore,True) #TODO: almost all code duplicated! #but parameter list is different so a simple function pointer wont work def cskHole(self,diameter, cskDiameter,cskAngle,depth=None):
[docs] """ Makes a countersunk hole for each item on the stack. :param diameter: the diameter of the hole :type diamter: float > 0 :param cskDiameter: the diameter of the countersink :type cskDiameter: float > 0 and > diameter :param cskAngle: angle of the countersink, in degrees ( 82 is common ) :type cskAngle: float > 0 :param depth: the depth of the hole :type depth: float > 0 or None to drill thru the entire part. The surface of the hole is at the current workplane. One hole is created for each item on the stack. A very common use case is to use a construction rectangle to define the centers of a set of holes, like so:: s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\ .vertices().cskHole(0.125, 0.25,82,depth=None) This sample creates a plate with a set of holes at the corners. **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming to create, but are quite easily defined by users. see :py:meth:`cboreHole` to make counterbores instead of countersinks """ if depth is None: depth = self.largestDimension() def _makeCsk(center): #center is in local coordinates boreDir = Vector(0,0,-1) #first make the hole hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coords! r = cskDiameter / 2.0 h = r / math.tan(math.radians(cskAngle / 2.0)) csk = Solid.makeCone(r,0.0,h,center,boreDir) r = hole.fuse(csk) return r return self.cutEach(_makeCsk,True) #TODO: almost all code duplicated! #but parameter list is different so a simple function pointer wont work def hole(self,diameter,depth=None):
[docs] """ Makes a hole for each item on the stack. :param diameter: the diameter of the hole :type diamter: float > 0 :param depth: the depth of the hole :type depth: float > 0 or None to drill thru the entire part. The surface of the hole is at the current workplane. One hole is created for each item on the stack. A very common use case is to use a construction rectangle to define the centers of a set of holes, like so:: s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\ .vertices().hole(0.125, 0.25,82,depth=None) This sample creates a plate with a set of holes at the corners. **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming to create, but are quite easily defined by users. see :py:meth:`cboreHole` and :py:meth:`cskHole` to make counterbores or countersinks """ if depth is None: depth = self.largestDimension() def _makeHole(center): """ Makes a single hole with counterbore at the supplied point returns a solid suitable for subtraction pnt is in local coordinates """ boreDir = Vector(0,0,-1) #first make the hole hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates! return hole return self.cutEach(_makeHole,True) def extrude(self,distance,combine=True):
[docs] """ Use all un-extruded wires in the parent chain to create a prismatic solid. :param distance: the distance to extrude, normal to the workplane plane :type distance: float, negative means opposite the normal direction :param boolean combine: True to combine the resulting solid with parent solids if found. :return: a CQ object with the resulting solid selected. extrude always *adds* material to a part. The returned object is always a CQ object, and depends on wither combine is True, and whether a context solid is already defined: * if combine is False, the new value is pushed onto the stack. * if combine is true, the value is combined with the context solid if it exists, and the resulting solid becomes the new context solid. FutureEnhancement: Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane extrude to surface. this is quite tricky since the surface selected may not be planar """ r = self._extrude(distance) #returns a Solid #by default, create and return a new solid (combine=False or no context solid) #here it is ok if there is no context solid baseSolid = self.findSolid(searchParents=True) if combine: if baseSolid is not None: r = baseSolid.fuse(r) baseSolid.wrapped = r.wrapped return self.newObject([r]) else: #if combine is false, we want a new object, and the new object should have its own #context solid return self.newObject([r]) #ns = self.newObject([r],False) #return ns def cut(self,toCut,combine=True):
[docs] """ Cuts the provided solid from the current solid, IE, perform a solid subtraction if combine=True, the result and the original are updated to point to the new object if combine=False, the result will be on the stack, but the original is unmodified :param toCut: object to cut :type toCut: a solid object, or a CQ object having a solid, :raises: ValueError if there is no solid to subtract from in the chain :return: a CQ object with the resulting object selected """ solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from if solidRef is None: raise ValueError("Cannot find solid to cut from!!!") solidToCut = None if type(toCut) == CQ or type(toCut) == Workplane: solidToCut = toCut.val() elif type(toCut) == Solid: solidToCut = toCut else: raise ValueError("Cannot cut Type '%s' " % str(type(toCut))) newS = solidRef.cut(solidToCut) if combine: solidRef.wrapped = newS.wrapped return self.newObject([newS]) def cutBlind(self,distanceToCut):
[docs] """ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. Similar to extrude, except that a solid in the parent chain is required to remove material from. cutBlind always removes material from a part. :param distanceToCut: distance to extrude before cutting :type distanceToCut: float, >0 means in the positive direction of the workplane normal, <0 means in the negative direction :raises: ValueError if there is no solid to subtract from in the chain :return: a CQ object with the resulting object selected see :py:meth:`cutThruAll` to cut material from the entire part Future Enhancements: Cut Up to Surface """ #first, make the object toCut = self._extrude(distanceToCut) #now find a solid in the chain solidRef = self.findSolid() s= solidRef.cut(toCut) solidRef.wrapped = s.wrapped return self.newObject([s]) def cutThruAll(self,positive=False):
[docs] """ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. Similar to extrude, except that a solid in the parent chain is required to remove material from. cutThruAll always removes material from a part. :param boolean positive: True to cut in the positive direction, false to cut in the negative direction :raises: ValueError if there is no solid to subtract from in the chain :return: a CQ object with the resulting object selected see :py:meth:`cutBlind` to cut material to a limited depth """ maxDim = self.largestDimension() if not positive: maxDim *= (-1.0) return self.cutBlind(maxDim) def loft(self,filled=True,combine=True):
[docs] """ Make a lofted solid, through the set of wires. :return: """ wiresToLoft = self.ctx.pendingWires self.ctx.pendingWires = [] r = Solid.makeLoft(wiresToLoft) if combine: parentSolid = self.findSolid(searchStack=False,searchParents=True) if parentSolid is not None: r = parentSolid.fuse(r) parentSolid.wrapped = r.wrapped return self.newObject([r]) def _extrude(self,distance):
""" Make a prismatic solid from the existing set of pending wires. :param distance: distance to extrude :return: a FreeCAD solid, suitable for boolean operations. This method is a utility method, primarily for plugin and internal use. It is the basis for cutBlind,extrude,cutThruAll, and all similar methods. Future Enhancements: extrude along a profile ( sweep ) """ #group wires together into faces based on which ones are inside the others #result is a list of lists wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires),self.plane,[]) self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion #compute extrusion vector and extrude eDir = self.plane.zDir.multiply(distance) #one would think that fusing faces into a compound and then extruding would work, #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc), #but then cutting it from the main solid fails with BRep_NotDone. #the work around is to extrude each and then join the resulting solids, which seems to work #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets r = None for ws in wireSets: thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) if r is None: r = thisObj else: r = r.fuse(thisObj) return Solid(r) def box(self,length,width,height,centered=(True,True,True),combine=True):
[docs] """ Return a 3d box with specified dimensions for each object on the stack. :param length: box size in X direction :type length: float > 0 :param width: box size in Y direction :type width: float > 0 :param height: box size in Z direction :type height: float > 0 :param centered: should the box be centered, or should reference point be at the lower bound of the range? :param combine: should the results be combined with other solids on the stack ( and each other)? :type combine: true to combine shapes, false otherwise. Centered is a tuple that describes whether the box should be centered on the x,y, and z axes. If true, the box is centered on the respective axis relative to the workplane origin, if false, the workplane center will represent the lower bound of the resulting box one box is created for each item on the current stack. If no items are on the stack, one box using the current workplane center is created. If combine is true, the result will be a single object on the stack: if a solid was found in the chain, the result is that solid with all boxes produced fused onto it otherwise, the result is the combination of all the produced boxes if combine is false, the result will be a list of the boxes produced Most often boxes form the basis for a part:: #make a single box with lower left corner at origin s = Workplane().box(1,2,3,centered=(False,False,False) But sometimes it is useful to create an array of them: #create 4 small square bumps on a larger base plate: s = Workplane().box(4,4,0.5).faces(">Z").workplane()\ .rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True) """ def _makebox(pnt): #(xp,yp,zp) = self.plane.toLocalCoords(pnt) (xp,yp,zp) = pnt.toTuple() if centered[0]: xp = xp-(length/2.0) if centered[1]: yp = yp-(width/2.0) if centered[2]: zp = zp-(height/2.0) return Solid.makeBox(length,width,height,Vector(xp,yp,zp)) boxes = self.eachpoint(_makebox,True) #if combination is not desired, just return the created boxes if not combine: return boxes else: #combine everything return boxes.combineSolids()