diff --git a/CadQuery/Libs/cadquery/CQ.py b/CadQuery/Libs/cadquery/CQ.py index ee1433e..d666659 100644 --- a/CadQuery/Libs/cadquery/CQ.py +++ b/CadQuery/Libs/cadquery/CQ.py @@ -17,9 +17,11 @@ License along with this library; If not, see """ -import time,math +import time, math from cadquery import * from cadquery import selectors +from cadquery import exporters + class CQContext(object): """ @@ -29,22 +31,25 @@ class CQContext(object): 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 + self.pendingWires = [] # a list of wires that have been created and need to be extruded + self.pendingEdges = [] # a list of created pending edges that need to be joined into wires + # a reference to the first point for a set of edges. + # Used to determine how to behave when close() is called + self.firstPoint = None + self.tolerance = 0.0001 # user specified tolerance + class CQ(object): """ 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 + using work planes, and 3d operations like fillets, shells, and splitting """ - def __init__(self,obj): + def __init__(self, obj): """ - Construct a new cadquery (CQ) object that wraps a CAD primitive. + 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 ) @@ -53,16 +58,14 @@ class CQ(object): self.ctx = CQContext() self.parent = None - if obj: #guarded because sometimes None for internal use + if obj: # guarded because sometimes None for internal use self.objects.append(obj) - def newObject(self,objlist): + def newObject(self, objlist): """ 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, @@ -71,23 +74,23 @@ class CQ(object): Custom plugins and subclasses should use this method to create new CQ objects correctly. """ - r = CQ(None) #create a completely blank one + r = CQ(None) # create a completely blank one r.parent = self - r.ctx = self.ctx #context solid remains the same + r.ctx = self.ctx # context solid remains the same r.objects = list(objlist) return r - def _collectProperty(self,propName): + 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. + Collects all of the values for propName, + for all items on the stack. + FreeCAD objects do not implement id correctly, + so hashCode is used to ensure we don't 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. + 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: @@ -95,17 +98,17 @@ class CQ(object): #tricky-- if an object is a compound of solids, #do not return all of the solids underneath-- typically #then we'll keep joining to ourself - if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() =='Compound': - for i in getattr(o,'Compounds')(): + if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() == 'Compound': + for i in getattr(o, 'Compounds')(): all[i.hashCode()] = i else: - if hasattr(o,propName): - for i in getattr(o,propName)(): + 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): + def split(self, keepTop=False, keepBottom=False): """ Splits a solid on the stack into two parts, optionally keeping the separate parts. @@ -115,13 +118,13 @@ class CQ(object): :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:: + 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() @@ -130,15 +133,15 @@ class CQ(object): 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) + 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]) + 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 @@ -149,21 +152,19 @@ class CQ(object): solid.wrapped = bottom.wrapped return self.newObject([bottom]) - - def combineSolids(self,otherCQToCombine=None): + def combineSolids(self, otherCQToCombine=None): """ - !!!DEPRECATED!!! use union() - Combines all solids on the current stack, and any context object, together - into a single object. + !!!DEPRECATED!!! use union() + 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. + 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. + :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 @@ -176,8 +177,8 @@ class CQ(object): 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 + #get context solid and we don't want to find our own objects + ctxSolid = self.findSolid(searchStack=False, searchParents=True) if ctxSolid is None: ctxSolid = toCombine.pop(0) @@ -199,14 +200,12 @@ class CQ(object): 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): """ Return the number of objects currently on the stack - """ return len(self.objects) @@ -218,26 +217,22 @@ class CQ(object): :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): + def add(self, obj): """ - adds an object or a list of objects to the stack + 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 - :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 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) @@ -258,16 +253,15 @@ class CQ(object): def toFreecad(self): """ - Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code needed when - rendering a model in FreeCAD's 3D view. + Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code + needed when rendering a model in FreeCAD's 3D view. :return: The wrapped FreeCAD object :rtype A FreeCAD object or a SolidReference """ return self.objects[0].wrapped - - def workplane(self,offset=0.0,invert=False): + def workplane(self, offset=0.0, invert=False): """ Creates a new 2-D workplane, located relative to the first face on the stack. @@ -278,38 +272,46 @@ class CQ(object): :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 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 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. + * 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. + 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. - + 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) + """ + Figures out the X direction based on the given normal. + :param :normal The direction that's normal to the plane. + :type :normal A Vector + :return A vector representing the X direction. + """ + 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) + xd = Vector(1, 0, 0) return xd faceToBuildOn = None @@ -322,18 +324,18 @@ class CQ(object): # xDir = _computeXdir(normal) # else: # raise ValueError("If a vertex is selected, a face must be the immediate parent") - if isinstance(obj,Face): + if isinstance(obj, Face): faceToBuildOn = obj center = obj.Center() normal = obj.normalAt(center) xDir = _computeXdir(normal) else: - if hasattr(obj,'Center'): + 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") + raise ValueError("Needs a face or a vertex or point on a work plane") #invert if requested if invert: @@ -360,7 +362,7 @@ class CQ(object): """ return self.newObject(self.objects[0:1]) - def item(self,i): + def item(self, i): """ Return the ith item on the stack. @@ -388,33 +390,31 @@ class CQ(object): 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): + def findSolid(self, searchStack=True, searchParents=True): """ 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 + :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. + 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. + 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!") @@ -424,11 +424,11 @@ class CQ(object): return s if searchParents and self.parent is not None: - return self.parent.findSolid(searchStack=True,searchParents=searchParents) + return self.parent.findSolid(searchStack=True, searchParents=searchParents) return None - def _selectObjects(self,objType,selector=None): + def _selectObjects(self, objType, selector=None): """ Filters objects of the selected type with the specified selector,and returns results @@ -436,11 +436,12 @@ class CQ(object): :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. + **Implementation Note**: This is the base implementation 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 + # A single list of all faces from all objects on the stack + toReturn = self._collectProperty(objType) if selector is not None: if type(selector) == str: @@ -451,17 +452,19 @@ class CQ(object): return self.newObject(toReturn) - def vertices(self,selector=None): + def vertices(self, selector=None): """ - 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. + 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 + :return: a CQ object who's 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 + 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:: @@ -478,19 +481,21 @@ class CQ(object): :py:class:`StringSyntaxSelector` """ - return self._selectObjects('Vertices',selector) + return self._selectObjects('Vertices', selector) - def faces(self,selector=None): + def faces(self, selector=None): """ - 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. + 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. + :return: a CQ object who's 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 + 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:: @@ -508,17 +513,18 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Faces',selector) + return self._selectObjects('Faces', selector) - def edges(self,selector=None): + def edges(self, selector=None): """ - 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. + 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. + :return: a CQ object who's 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 @@ -538,17 +544,18 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Edges',selector) + return self._selectObjects('Edges', selector) - def wires(self,selector=None): + def wires(self, selector=None): """ - 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. + 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. + :return: a CQ object who's 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 @@ -560,17 +567,18 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Wires',selector) + return self._selectObjects('Wires', selector) - def solids(self,selector=None): + def solids(self, selector=None): """ - 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. + 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. + :return: a CQ object who's 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 @@ -580,96 +588,97 @@ class CQ(object): 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. + 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) + return self._selectObjects('Solids', selector) - def shells(self,selector=None): + def shells(self, selector=None): """ - 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. + 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. + :return: a CQ object who's 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. + 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) + return self._selectObjects('Shells', selector) - def compounds(self,selector=None): + def compounds(self, selector=None): """ - 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. + 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. + :return: a CQ object who's 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. + 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) + return self._selectObjects('Compounds', selector) - def toSvg(self,opts=None): + def toSvg(self, opts=None): """ - Returns svg text that represents the first item on the stack. + Returns svg text that represents the first item on the stack. - for testing purposes. + for testing purposes. - :param options: svg formatting options - :type options: dictionary, width and height - :return: a string that contains SVG that represents this item. + :param opts: svg formatting options + :type opts: dictionary, width and height + :return: a string that contains SVG that represents this item. """ - return SVGexporter.getSVG(self.val().wrapped,opts) + return exporters.getSVG(self.val().wrapped, opts) - def exportSvg(self,fileName): + def exportSvg(self, fileName): """ - Exports the first item on the stack as an SVG file + 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 + For testing purposes mainly. + :param fileName: the filename to export + :type fileName: String, absolute path to the file """ - exporters.exportSVG(self,fileName) + exporters.exportSVG(self, fileName) - def rotateAboutCenter(self,axisEndPoint,angleDegrees): + def rotateAboutCenter(self, axisEndPoint, angleDegrees): """ - Rotates all items on the stack by the specified angle, about the specified axis + 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. + 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. + :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. + 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, because 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 @@ -678,12 +687,14 @@ class CQ(object): def _rot(obj): startPt = obj.Center() endPt = startPt + endVec - return obj.rotate(startPt,endPt,angleDegrees) + return obj.rotate(startPt, endPt, angleDegrees) return self.each(_rot, False) def rotate(self, axisStartPoint, axisEndPoint, angleDegrees): - """Returns a copy of all of the items on the stack rotated through and angle around the axis of rotation. + """ + Returns a copy of all of the items on the stack rotated through and angle around the axis + of rotation. :param axisStartPoint: The first point of the axis of rotation :type axisStartPoint: a 3-tuple of floats @@ -692,182 +703,174 @@ class CQ(object): :param angleDegrees: the rotation angle, in degrees :type angleDegrees: 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.rotate(axisStartPoint, axisEndPoint, angleDegrees) for o in self.objects]) + return self.newObject([o.rotate(axisStartPoint, axisEndPoint, angleDegrees) + for o in self.objects]) - def translate(self,vec): + def translate(self, vec): """ - Returns a copy of all of the items on the stack moved by the specified translation vector. + Returns a copy of all of the items on the stack moved by the specified translation vector. - :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. + :param tupleDistance: distance to move, in global coordinates + :type tupleDistance: a 3-tuple of float + :returns: a CQ object """ return self.newObject([o.translate(vec) for o in self.objects]) - def shell(self,thickness): + def shell(self, thickness): """ - Remove the selected faces to create a shell of the specified thickness. + 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. + 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. + :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:: + 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) + 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:: + 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)) + 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 + 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 + **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 - + 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") + raise ValueError("Shelling requires that faces be selected") - s = solidRef.shell(self.objects,thickness) + s = solidRef.shell(self.objects, thickness) solidRef.wrapped = s.wrapped return self.newObject([s]) - - def fillet(self,radius): + def fillet(self, radius): """ - Fillets a solid on the selected edges. + 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. + 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. + :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:: + This example will create a unit cube, with the top edges filleted:: - s = Workplane().box(1,1,1).faces("+Z").edges().fillet(0.1) + 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 + # 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, + # TODO: we segfault solid = self.findSolid() edgeList = self.edges().vals() if len(edgeList) < 1: - raise ValueError ("Fillets requires that edges be selected") + raise ValueError("Fillets requires that edges be selected") - s = solid.fillet(radius,edgeList) + s = solid.fillet(radius, edgeList) solid.wrapped = s.wrapped return self.newObject([s]) + class Workplane(CQ): """ - Defines a coordinate system in space, in which 2-d coordinates can be used. + 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 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 inPlane: the plane in which the workplane will be done + :type inPlane: 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 + :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. - - .. 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. + 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) + tmpPlane = Plane.named(inPlane, origin) else: tmpPlane = None - if tmpPlane == None: + if tmpPlane is 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 + # Changed so that workplane has the center as the first item on the stack + self.objects = [self.plane.origin] self.parent = None self.ctx = CQContext() - def transformed(self,rotate=(0,0,0),offset=(0,0,0)): + def transformed(self, rotate=(0, 0, 0), offset=(0, 0, 0)): """ 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 + 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: 3-tuple of angles to rotate, in degrees relative to work plane coordinates :param offset: 3-tuple to offset the new plane, in local work plane coordinates :return: a new work plane, transformed as requested @@ -881,23 +884,22 @@ class Workplane(CQ): offset = offset.toTuple() p = self.plane.rotated(rotate) - p.setOrigin3d(self.plane.toWorldCoords(offset )) + p.setOrigin3d(self.plane.toWorldCoords(offset)) ns = self.newObject([p.origin]) ns.plane = p return ns - def newObject(self,objlist): + def newObject(self, objlist): """ - Create a new workplane object from this one. + 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. + 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 @@ -908,33 +910,33 @@ class Workplane(CQ): ns.ctx = self.ctx return ns - def _findFromPoint(self,useLocalCoords=False): + 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 + 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. + :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. + :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 + 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. + WARNING: only the first object on the stack is used. - NOTE: + NOTE: """ obj = self.objects[0] p = None - if isinstance(obj,Edge): + if isinstance(obj, Edge): p = obj.endPoint() - elif isinstance(obj,Vector): - p = obj + elif isinstance(obj, Vector): + p = obj else: raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) @@ -943,27 +945,27 @@ class Workplane(CQ): else: return p - def rarray(self,xSpacing,ySpacing,xCount,yCount,center=True): + def rarray(self, xSpacing, ySpacing, xCount, yCount, center=True): """ - Creates an array of points and pushes them onto the stack. - If you want to position the array at another point, create another workplane - that is shifted to the position you would like to use as a reference + Creates an array of points and pushes them onto the stack. + If you want to position the array at another point, create another workplane + that is shifted to the position you would like to use as a reference - :param xSpacing: spacing between points in the x direction ( must be > 0) - :param ySpacing: spacing between points in the y direction ( must be > 0) - :param xCount: number of points ( > 0 ) - :param yCount: number of poitns ( > 0 ) - :param center: if true, the array will be centered at the center of the workplane. if false, the lower - left corner will be at the center of the work plane + :param xSpacing: spacing between points in the x direction ( must be > 0) + :param ySpacing: spacing between points in the y direction ( must be > 0) + :param xCount: number of points ( > 0 ) + :param yCount: number of points ( > 0 ) + :param center: if true, the array will be centered at the center of the workplane. if + false, the lower left corner will be at the center of the work plane """ if xSpacing < 1 or ySpacing < 1 or xCount < 1 or yCount < 1: raise ValueError("Spacing and count must be > 0 ") - lpoints = [] #coordinates relative to bottom left point + lpoints = [] # coordinates relative to bottom left point for x in range(xCount): for y in range(yCount): - lpoints.append( (xSpacing*(x), ySpacing*(y)) ) + lpoints.append((xSpacing * x, ySpacing * y)) #shift points down and left relative to origin if requested if center: @@ -971,29 +973,30 @@ class Workplane(CQ): yc = ySpacing*(yCount-1) * 0.5 cpoints = [] for p in lpoints: - cpoints.append( ( p[0] - xc, p[1] - yc )) + cpoints.append((p[0] - xc, p[1] - yc)) lpoints = list(cpoints) return self.pushPoints(lpoints) - def pushPoints(self,pntList): + def pushPoints(self, pntList): """ - Pushes a list of points onto the stack as vertices. - The points are in the 2-d coordinate space of the workplane face + 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. + :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:: + 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. + 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: @@ -1002,54 +1005,52 @@ class Workplane(CQ): return self.newObject(vecs) - def center(self,x,y): + def center(self, x, y): """ - Shift local coordinates to the specified location. + Shift local coordinates to the specified location. - The location is specified in terms of local coordinates. + 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. + :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. + 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:: + 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() + #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 + 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) + self.plane.setOrigin2d(x, y) n = self.newObject([self.plane.origin]) return n - def lineTo(self, x, y,forConstruction=False): + def lineTo(self, x, y, forConstruction=False): """ - Make a line from the current point to the provided point + 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. + :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) + p = Edge.makeLine(startPoint, endPoint) if not forConstruction: self._addPendingEdge(p) @@ -1057,141 +1058,134 @@ class Workplane(CQ): return self.newObject([p]) #line a specified incremental amount from current point - def line(self, xDist, yDist ,forConstruction=False): + def line(self, xDist, yDist, forConstruction=False): """ - Make a line from the current point to the provided point, using - dimensions relative to the current point + 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. + :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) + p = self._findFromPoint(True) # return local coordinates + return self.lineTo(p.x + xDist, yDist + p.y, forConstruction) - def vLine(self, distance,forConstruction=False): + def vLine(self, distance, forConstruction=False): """ - 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 + 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) + return self.line(0, distance, forConstruction) - def vLineTo(self,yCoord,forConstruction=False): + def hLine(self, distance, forConstruction=False): """ - Make a vertcial line from the current point to the provided y coordinate. + Make a horizontal line from the current point the provided distance - Useful if it is more convienient to specify the end location rather than distance, - as in :py:meth:`vLine` + :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) - :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 + def vLineTo(self, yCoord, forConstruction=False): + """ + Make a vertical line from the current point to the provided y coordinate. + Useful if it is more convenient 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) + return self.lineTo(p.x, yCoord, forConstruction) - def hLineTo(self,xCoord,forConstruction=False): + def hLineTo(self, xCoord, forConstruction=False): """ - Make a horizontal line from the curren tpoint to the provided x coordinate. + Make a horizontal line from the current point 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 + Useful if it is more convenient 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): - """ - 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) + return self.lineTo(xCoord, p.y, forConstruction) #absolute move in current plane, not drawing def moveTo(self, x=0, y=0): """ - Move to the specified point, without drawing. + 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. + :param x: desired x location, in local coordinates + :type x: float, or none for zero + :param y: desired y location, in local coordinates + :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 ). + 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 + See :py:meth:`move` to do the same thing but using relative dimensions """ - newCenter = Vector(x,y,0) + newCenter = Vector(x, y, 0) return self.newObject([self.plane.toWorldCoords(newCenter)]) #relative move in current plane, not drawing def move(self, xDist=0, yDist=0): """ - Move the specified distance from the current point, without drawing. + 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. + :param xDist: desired x distance, in local coordinates + :type xDist: float, or none for zero + :param yDist: desired y distance, in local coordinates + :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 ). + 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 + See :py:meth:`moveTo` to do the same thing but using absolute coordinates """ p = self._findFromPoint(True) - newCenter = p + Vector(xDist,yDist,0) + newCenter = p + Vector(xDist, yDist, 0) return self.newObject([self.plane.toWorldCoords(newCenter)]) - def spline(self, listOfXYTuple, forConstruction=False): """ - Create a spline interpolated through the provided points. + 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 + :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 + The spline will begin at the current point, and + end with the last point in the XY tuple list - This example creates a block with a spline for one side:: + 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) + 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 + *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]) @@ -1206,53 +1200,50 @@ class Workplane(CQ): return self.newObject([e]) - def threePointArc(self,point1, point2,forConstruction=False): + def threePointArc(self, point1, point2, forConstruction=False): """ - Draw an arc from the current point, through point1, and ending at point2 + 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 + :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 Enhancements: + 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) + arc = Edge.makeThreePointArc(gstartPoint, gpoint1, gpoint2) if not forConstruction: self._addPendingEdge(arc) return self.newObject([arc]) - def rotateAndCopy(self,matrix): + def rotateAndCopy(self, matrix): """ - 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. + 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. + :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 + 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 """ #convert edges to a wire, if there are pending edges @@ -1274,21 +1265,21 @@ class Workplane(CQ): def mirrorY(self): """ - Mirror entities around the y axis of the workplane plane. + 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. + :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 + 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:: + 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) + s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25) - Produces a flat, heart shaped object + Produces a flat, heart shaped object - Future Enhancements: - mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness + Future Enhancements: + mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness """ tm = Matrix() tm.rotateY(math.pi) @@ -1296,25 +1287,25 @@ class Workplane(CQ): def mirrorX(self): """ - Mirror entities around the x axis of the workplane plane. + 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. + :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 + 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. + Typically used to make creating wires with symmetry easier. - Future Enhancements: - mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness + Future Enhancements: + mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness """ tm = Matrix() tm.rotateX(math.pi) return self.rotateAndCopy(tm) - def _addPendingEdge(self,edge): + def _addPendingEdge(self, edge): """ - Queues an edge for later combination into a wire. + Queues an edge for later combination into a wire. :param edge: :return: @@ -1324,33 +1315,32 @@ class Workplane(CQ): if self.ctx.firstPoint is None: self.ctx.firstPoint = self.plane.toLocalCoords(edge.startPoint()) - def _addPendingWire(self,wire): + def _addPendingWire(self, wire): """ - Queue a Wire for later extrusion + Queue a Wire for later extrusion - Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids. + 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. + 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. + 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 automatically combines them into faces - when necessary to make a solid. + Similarly, cadQuery tracks pending wires, and automatically combines them into faces + when necessary to make a solid. """ self.ctx.pendingWires.append(wire) - def consolidateWires(self): """ - 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 + 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 sometimes - Additionally, it has a bug where a profile composed of two wires ( rather than one ) - also does not work properly. Together these are a real problem. + FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly sometimes + Additionally, it has a bug where a profile composed of two wires ( rather than one ) + also does not work properly. Together these are a real problem. """ wires = self.wires().vals() if len(wires) < 2: @@ -1370,25 +1360,25 @@ class Workplane(CQ): r._addPendingWire(w) return r - - - def wire(self,forConstruction=False): + def wire(self, forConstruction=False): """ - Returns a CQ object with all pending edges connected into a wire. + 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 + 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 + :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 + This method is primarily of use to plugin developers making utilities for 2-d construction. + This method should 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 + SEE '2-d construction concepts' for a more detailed explanation of how CadQuery handles + edges, wires, etc - Any non edges will still remain. + Any non edges will still remain. """ edges = self.ctx.pendingEdges @@ -1411,37 +1401,37 @@ class Workplane(CQ): return self.newObject(others + [w]) - def each(self,callBackFunction,useLocalCoordinates=False): + def each(self, callBackFunction, useLocalCoordinates=False): """ - runs the provided function on each value in the stack, and collects the return values into a new CQ - object. + 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 + 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 + :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 + 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. + 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 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. + 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 coordinates 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. + 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 - + TODO: wrapper object for Wire will clean up forConstruction flag everywhere """ results = [] for obj in self.objects: @@ -1457,28 +1447,26 @@ class Workplane(CQ): if not r.forConstruction: self._addPendingWire(r) - results.append ( r ) - + results.append(r) return self.newObject(results) - def eachpoint(self,callbackFunction, useLocalCoordinates=False): + def eachpoint(self, callbackFunction, useLocalCoordinates=False): """ - Same as each(), except each item on the stack is converted into a point before it - is passed into the callback function. + 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. + :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 + :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 + 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 = [] @@ -1491,150 +1479,150 @@ class Workplane(CQ): for v in self.objects: pnts.append(v.Center()) - return self.newObject(pnts).each(callbackFunction,useLocalCoordinates ) + return self.newObject(pnts).each(callbackFunction, useLocalCoordinates) - - #make a rectangle - def rect(self,xLen,yLen,centered=True,forConstruction=False): + def rect(self, xLen, yLen, centered=True, forConstruction=False): """ - Make a rectangle for each item on the stack. + 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 + :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:: + 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) + 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 + 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() + # 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) ) + 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)) + 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) + w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction) return w #return Part.makePolygon([p1,p2,p3,p4,p1]) - return self.eachpoint(makeRectangleWire,True) + return self.eachpoint(makeRectangleWire, True) #circle from current point - def circle(self,radius,forConstruction=False): + def circle(self, radius, forConstruction=False): """ - Make a circle for each item on the stack. + 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 + :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:: + 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) + 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:: + 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) + s = Workplane().circle(2.0).circle(1.0) - Creates two concentric circles, which when extruded will form a ring. + 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 + 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 = Wire.makeCircle(radius, obj, Vector(0, 0, 1)) cir.forConstruction = forConstruction return cir - return self.eachpoint(makeCircleWire,useLocalCoordinates=True) + return self.eachpoint(makeCircleWire, useLocalCoordinates=True) - def polygon(self,nSides,diameter): + def polygon(self, nSides, diameter): """ - Creates a polygon incribed in a circle of the specified diamter for each point on the stack + Creates a polygon inscribed in a circle of the specified diameter 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 + :param diameter: the size of the circle the polygon is inscribed into :return: a polygon wire - - """ def _makePolygon(center): #pnt is a vector in local coordinates - angle = 2.0 *math.pi / nSides + 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)) + 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) + return self.eachpoint(_makePolygon, True) - def polyline(self,listOfXYTuple,forConstruction=False): + def polyline(self, listOfXYTuple, forConstruction=False): """ - Create a polyline from a list of points + 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 + :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. + *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 + Future Enhancement: + 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 ] + 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): """ - End 2-d construction, and attempt to build a closed wire. + End 2-d construction, and attempt to build a closed wire. - :return: a CQ object with a completed wire on the stack, if possible. + :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. + 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) + 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() @@ -1644,31 +1632,32 @@ class Workplane(CQ): 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: + :return: A value representing the largest dimension of the first solid on the stack """ #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. + #TODO: 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 + return -1 - def cutEach(self,fcn,useLocalCoords=False): + def cutEach(self, fcn, useLocalCoords=False): """ - Evaluates the provided function at each point on the stack ( ie, eachpoint ) + 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 fcn: 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!") + 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() + results = self.eachpoint(fcn, useLocalCoords).vals() s = ctxSolid for cb in results: s = s.cut(cb) @@ -1677,83 +1666,85 @@ class Workplane(CQ): return self.newObject([s]) #but parameter list is different so a simple function pointer wont work - def cboreHole(self,diameter,cboreDiameter,cboreDepth,depth=None): + def cboreHole(self, diameter, cboreDiameter, cboreDepth, depth=None): """ - Makes a counterbored hole for each item on the stack. + 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. + :param diameter: the diameter of the hole + :type diameter: 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. + 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:: + 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) + 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. + 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. + **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 + 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 + 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) + boreDir = Vector(0, 0, -1) #first make the hole - hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates! + hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coordianates! #add the counter bore - cbore = Solid.makeCylinder(cboreDiameter/2.0,cboreDepth,center,boreDir) + cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir) r = hole.fuse(cbore) return r - return self.cutEach(_makeCbore,True) + 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): + def cskHole(self, diameter, cskDiameter, cskAngle, depth=None): """ - Makes a countersunk hole for each item on the stack. + 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. + :param diameter: the diameter of the hole + :type diameter: 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. + 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:: + 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) + 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. + 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. + **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 + see :py:meth:`cboreHole` to make counterbores instead of countersinks """ if depth is None: @@ -1762,98 +1753,100 @@ class Workplane(CQ): def _makeCsk(center): #center is in local coordinates - boreDir = Vector(0,0,-1) + boreDir = Vector(0, 0, -1) #first make the hole - hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coords! + 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) + csk = Solid.makeCone(r, 0.0, h, center, boreDir) r = hole.fuse(csk) return r - return self.cutEach(_makeCsk,True) - + 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): + def hole(self, diameter, depth=None): """ - Makes a hole for each item on the stack. + 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. + :param diameter: the diameter of the hole + :type diameter: 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. + 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:: + 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) + 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. + 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. + **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 + 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 + 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) + boreDir = Vector(0, 0, -1) #first make the hole - hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates! + hole = Solid.makeCylinder(diameter / 2.0, depth, center, boreDir) # local coordinates! return hole - return self.cutEach(_makeHole,True) + return self.cutEach(_makeHole, True) #TODO: duplicated code with _extrude and extrude - def twistExtrude(self,distance,angleDegrees,combine=True): + def twistExtrude(self, distance, angleDegrees, combine=True): """ - Extrudes a wire in the direction normal to the plane, but also twists by the specified angle over the - length of the extrusion + Extrudes a wire in the direction normal to the plane, but also twists by the specified + angle over the length of the extrusion - The center point of the rotation will be the center of the workplane + The center point of the rotation will be the center of the workplane - See extrude for more details, since this method is the same except for the the addition of the angle. - in fact, if angle=0, the result is the same as a linear extrude. + See extrude for more details, since this method is the same except for the the addition + of the angle. In fact, if angle=0, the result is the same as a linear extrude. - **NOTE** This method can create complex calculations, so be careful using it with complex geometries - - :param distance: the distance to extrude normal to the workplane - :param angle: angline ( in degrees) to rotate through the extrusion - :param boolean combine: True to combine the resulting solid with parent solids if found. - :return: a CQ object with the resulting solid selected. + **NOTE** This method can create complex calculations, so be careful using it with + complex geometries + :param distance: the distance to extrude normal to the workplane + :param angle: angline ( in degrees) to rotate through the extrusion + :param boolean combine: True to combine the resulting solid with parent solids if found. + :return: a CQ object with the resulting solid selected. """ #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,[]) + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) - self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion + 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 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 + #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.extrudeLinearWithRotation(ws[0],ws[1:],self.plane.origin, eDir,angleDegrees) + thisObj = Solid.extrudeLinearWithRotation(ws[0], ws[1:], self.plane.origin, + eDir, angleDegrees) if r is None: r = thisObj else: @@ -1864,29 +1857,30 @@ class Workplane(CQ): else: return self.newObject([r]) - def extrude(self,distance,combine=True): + def extrude(self, distance, combine=True): """ - Use all un-extruded wires in the parent chain to create a prismatic solid. + 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. + :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. + 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: + 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. + * 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 + 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 ( or a compound if there were multiple ) + r = self._extrude(distance) # returns a Solid (or a compound if there were multiple) if combine: return self._combineWithBase(r) else: @@ -1894,76 +1888,63 @@ class Workplane(CQ): def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True): """ - Use all un-revolved wires in the parent chain to create a solid. + Use all un-revolved wires in the parent chain to create a solid. - :param angleDegrees: the angle to revolve through. - :type angleDegrees: float, anything less than 360 degrees will leave the shape open - :param axisStart: the start point of the axis of rotation - :type axisStart: tuple, a two tuple - :param axisEnd: the end point of the axis of rotation - :type axisEnd: tuple, a two tuple - :param combine: True to combine the resulting solid with parent solids if found. - :type combine: boolean, combine with parent solid - :return: a CQ object with the resulting solid selected. + :param angleDegrees: the angle to revolve through. + :type angleDegrees: float, anything less than 360 degrees will leave the shape open + :param axisStart: the start point of the axis of rotation + :type axisStart: tuple, a two tuple + :param axisEnd: the end point of the axis of rotation + :type axisEnd: tuple, a two tuple + :param combine: True to combine the resulting solid with parent solids if found. + :type combine: boolean, combine with parent solid + :return: a CQ object with the resulting solid selected. - The returned object is always a CQ object, and depends on wither combine is True, and - whether a context solid is already defined: + 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. + * 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. """ #Make sure we account for users specifying angles larger than 360 degrees - angleDegrees = angleDegrees % 360.0 + angleDegrees %= 360.0 #Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees - #The default start point of the vector defining the axis of rotation will be the origin of the workplane + # The default start point of the vector defining the axis of rotation will be the origin + # of the workplane if axisStart is None: - axisStart = self.plane.toWorldCoords((0,0)).toTuple() + axisStart = self.plane.toWorldCoords((0, 0)).toTuple() else: axisStart = self.plane.toWorldCoords(axisStart).toTuple() - #The default end point of the vector defining the axis of rotation should be along the normal from the plane + # The default end point of the vector defining the axis of rotation should be along the + # normal from the plane if axisEnd is None: - #Make sure we match the user's assumed axis of rotation if they specified an start but not an end + # Make sure we match the user's assumed axis of rotation if they specified an start + # but not an end if axisStart[1] != 0: - axisEnd = self.plane.toWorldCoords((0,axisStart[1])).toTuple() + axisEnd = self.plane.toWorldCoords((0, axisStart[1])).toTuple() else: - axisEnd = self.plane.toWorldCoords((0,1)).toTuple() + axisEnd = self.plane.toWorldCoords((0, 1)).toTuple() else: axisEnd = self.plane.toWorldCoords(axisEnd).toTuple() - r = self._revolve(angleDegrees, axisStart, axisEnd) # returns a Solid ( or a compound if there were multiple ) + # returns a Solid (or a compound if there were multiple) + r = self._revolve(angleDegrees, axisStart, axisEnd) if combine: return self._combineWithBase(r) else: return self.newObject([r]) - def _combineWithBase2(self,obj): + def _combineWithBase(self, obj): """ - Combines the provided object with the base solid, if one can be found. - :param obj: - :return: a new object that represents the result of combining the base object with obj, - or obj if one could not be found - - """ - baseSolid = self.findSolid(searchParents=True) - r = obj - if baseSolid is not None: - r = baseSolid.fuse(obj) - baseSolid.wrapped = r.wrapped - - return self.newObject([r]) - - def _combineWithBase(self,obj): - """ - Combines the provided object with the base solid, if one can be found. - :param obj: - :return: a new object that represents the result of combining the base object with obj, - or obj if one could not be found - + Combines the provided object with the base solid, if one can be found. + :param obj: + :return: a new object that represents the result of combining the base object with obj, + or obj if one could not be found """ baseSolid = self.findSolid(searchParents=True) r = obj @@ -1975,11 +1956,11 @@ class Workplane(CQ): def combine(self): """ - Attempts to combine all of the items on the items on the stack into a single item. - WARNING: all of the items must be of the same type! + Attempts to combine all of the items on the stack into a single item. + WARNING: all of the items must be of the same type! - :raises: ValueError if there are no items on the stack, or if they cannot be combined - :return: a CQ object with the resulting object selected + :raises: ValueError if there are no items on the stack, or if they cannot be combined + :return: a CQ object with the resulting object selected """ items = list(self.objects) s = items.pop(0) @@ -1988,13 +1969,12 @@ class Workplane(CQ): return self.newObject([s]) - def union(self,toUnion=None,combine=True): + def union(self, toUnion=None, combine=True): """ - Unions all of the items on the stack of toUnion with the current solid. - If there is no current solid, the items in toUnion are unioned together. - 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 - + Unions all of the items on the stack of toUnion with the current solid. + If there is no current solid, the items in toUnion are unioned together. + 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 toUnion: :type toUnion: a solid object, or a CQ object having a solid, @@ -2005,7 +1985,7 @@ class Workplane(CQ): #first collect all of the items together if type(toUnion) == CQ or type(toUnion) == Workplane: solids = toUnion.solids().vals() - if len(solids) < 1 : + if len(solids) < 1: raise ValueError("CQ object must have at least one solid on the stack to union!") newS = solids.pop(0) for s in solids: @@ -2016,7 +1996,8 @@ class Workplane(CQ): raise ValueError("Cannot union Type '%s' " % str(type(toUnion))) #now combine with existing solid, if there is one - solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from + # look for parents to cut from + solidRef = self.findSolid(searchStack=True, searchParents=True) if combine and solidRef is not None: t = solidRef.fuse(newS) solidRef.wrapped = newS.wrapped @@ -2024,24 +2005,24 @@ class Workplane(CQ): else: return self.newObject([newS]) - def cut(self,toCut,combine=True): + def cut(self, toCut, combine=True): """ - Cuts the provided solid from the current solid, IE, perform a solid subtraction + 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 + 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 + # look for parents to cut from + solidRef = self.findSolid(searchStack=True, searchParents=True) if solidRef is None: - raise ValueError("Cannot find solid to cut from!!!") + raise ValueError("Cannot find solid to cut from!!!") solidToCut = None if type(toCut) == CQ or type(toCut) == Workplane: solidToCut = toCut.val() @@ -2055,23 +2036,23 @@ class Workplane(CQ): solidRef.wrapped = newS.wrapped return self.newObject([newS]) - - def cutBlind(self,distanceToCut): + def cutBlind(self, distanceToCut): """ - Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. + 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. + 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 + :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 + see :py:meth:`cutThruAll` to cut material from the entire part - Future Enhancements: - Cut Up to Surface + Future Enhancements: + Cut Up to Surface """ #first, make the object toCut = self._extrude(distanceToCut) @@ -2080,23 +2061,23 @@ class Workplane(CQ): solidRef = self.findSolid() - s= solidRef.cut(toCut) + s = solidRef.cut(toCut) solidRef.wrapped = s.wrapped return self.newObject([s]) - def cutThruAll(self,positive=False): + def cutThruAll(self, positive=False): """ - Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. + 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. + 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 + :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: @@ -2104,72 +2085,70 @@ class Workplane(CQ): return self.cutBlind(maxDim) - - def loft(self,filled=True,combine=True): + def loft(self, filled=True, ruled=False, combine=True): """ - Make a lofted solid, through the set of wires. - :return: + Make a lofted solid, through the set of wires. + :return: a CQ object containing the created loft """ wiresToLoft = self.ctx.pendingWires self.ctx.pendingWires = [] - r = Solid.makeLoft(wiresToLoft) + r = Solid.makeLoft(wiresToLoft, ruled) if combine: - parentSolid = self.findSolid(searchStack=False,searchParents=True) + 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): + def _extrude(self, distance): """ - Make a prismatic solid from the existing set of pending wires. + Make a prismatic solid from the existing set of pending wires. - :param distance: distance to extrude - :return: a FreeCAD solid, suitable for boolean operations. + :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. + 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 ) + 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 s = time.time() - wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires),self.plane,[]) + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) #print "sorted wires in %d sec" % ( time.time() - s ) - self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion + 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 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 + # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are + # multiple sets - # IMPORTANT NOTE: OCC is slow slow slow in boolean operations. So you do NOT want to fuse each item to - # another and save the result-- instead, you want to combine all of the new items into a compound, and fuse - # them together!!! - """ - r = None - for ws in wireSets: - thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) - if r is None: - r = thisObj - else: - s = time.time() - r = r.fuse(thisObj) - print "Fused in %0.3f sec" % ( time.time() - s ) - return r - """ + # IMPORTANT NOTE: OCC is slow slow slow in boolean operations. So you do NOT want to fuse + # each item to another and save the result-- instead, you want to combine all of the new + # items into a compound, and fuse them together!!! + # r = None + # for ws in wireSets: + # thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) + # if r is None: + # r = thisObj + # else: + # s = time.time() + # r = r.fuse(thisObj) + # print "Fused in %0.3f sec" % ( time.time() - s ) + # return r toFuse = [] for ws in wireSets: @@ -2180,20 +2159,20 @@ class Workplane(CQ): def _revolve(self, angleDegrees, axisStart, axisEnd): """ - Make a solid from the existing set of pending wires. + Make a solid from the existing set of pending wires. - :param angleDegrees: the angle to revolve through. - :type angleDegrees: float, anything less than 360 degrees will leave the shape open - :param axisStart: the start point of the axis of rotation - :type axisStart: tuple, a two tuple - :param axisEnd: the end point of the axis of rotation - :type axisEnd: tuple, a two tuple - :return: a FreeCAD solid, suitable for boolean operations. + :param angleDegrees: the angle to revolve through. + :type angleDegrees: float, anything less than 360 degrees will leave the shape open + :param axisStart: the start point of the axis of rotation + :type axisStart: tuple, a two tuple + :param axisEnd: the end point of the axis of rotation + :type axisEnd: tuple, a two tuple + :return: a FreeCAD solid, suitable for boolean operations. - This method is a utility method, primarily for plugin and internal use. + This method is a utility method, primarily for plugin and internal use. """ #We have to gather the wires to be revolved - wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires),self.plane,[]) + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) #Mark that all of the wires have been used to create a revolution self.ctx.pendingWires = [] @@ -2206,7 +2185,7 @@ class Workplane(CQ): return Compound.makeCompound(toFuse) - def box(self,length,width,height,centered=(True,True,True),combine=True): + def box(self, length, width, height, centered=(True, True, True), combine=True): """ Return a 3d box with specified dimensions for each object on the stack. @@ -2216,20 +2195,22 @@ class Workplane(CQ): :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)? + :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 + 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. + 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 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 @@ -2249,17 +2230,17 @@ class Workplane(CQ): def _makebox(pnt): #(xp,yp,zp) = self.plane.toLocalCoords(pnt) - (xp,yp,zp) = pnt.toTuple() + (xp, yp, zp) = pnt.toTuple() if centered[0]: - xp = xp-(length/2.0) + xp -= (length / 2.0) if centered[1]: - yp = yp-(width/2.0) + yp -= (width / 2.0) if centered[2]: - zp = zp-(height/2.0) + zp -= (height / 2.0) - return Solid.makeBox(length,width,height,Vector(xp,yp,zp)) + return Solid.makeBox(length, width, height, Vector(xp, yp, zp)) - boxes = self.eachpoint(_makebox,True) + boxes = self.eachpoint(_makebox, True) #if combination is not desired, just return the created boxes if not combine: @@ -2268,7 +2249,8 @@ class Workplane(CQ): #combine everything return self.union(boxes) - def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, centered=(True, True, True), combine=True): + def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, + centered=(True, True, True), combine=True): """ Returns a 3D sphere with the specified radius for each point on the stack @@ -2282,21 +2264,24 @@ class Workplane(CQ): :type angle2: float > 0 :param angle3: The third angle to sweep the sphere arc through :type angle3: float > 0 - :param centered: A three-tuple of booleans that determines whether the sphere is centered on each axis origin - :param combine: Whether the results should be combined with other solids on the stack (and each other) + :param centered: A three-tuple of booleans that determines whether the sphere is centered + on each axis origin + :param combine: Whether the results should be combined with other solids on the stack + (and each other) :type combine: true to combine shapes, false otherwise :return: A sphere object for each point on the stack - Centered is a tuple that describes whether the sphere should be centered on the x,y, and z axes. If true, - the sphere is centered on the respective axis relative to the workplane origin, if false, the workplane center - will represent the lower bound of the resulting sphere + Centered is a tuple that describes whether the sphere should be centered on the x,y, and + z axes. If true, the sphere is centered on the respective axis relative to the workplane + origin, if false, the workplane center will represent the lower bound of the resulting + sphere. - One sphere 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. + One sphere 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 spheres produced fused onto it - otherwise, the result is the combination of all the produced boxes + If a solid was found in the chain, the result is that solid with all spheres 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 spheres produced """ @@ -2314,11 +2299,11 @@ class Workplane(CQ): (xp, yp, zp) = pnt.toTuple() if centered[0]: - xp = xp - radius + xp -= radius if centered[1]: - yp = yp - radius + yp -= radius if centered[2]: - zp = zp - radius + zp -= radius return Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3) diff --git a/CadQuery/Libs/cadquery/freecad_impl/exporters.py b/CadQuery/Libs/cadquery/freecad_impl/exporters.py index 6119200..8ba681b 100644 --- a/CadQuery/Libs/cadquery/freecad_impl/exporters.py +++ b/CadQuery/Libs/cadquery/freecad_impl/exporters.py @@ -249,6 +249,7 @@ def getPaths(freeCadSVG): else: return ([],[]) + def getSVG(shape,opts=None): """ Export a shape to SVG diff --git a/CadQuery/Libs/cadquery/freecad_impl/shapes.py b/CadQuery/Libs/cadquery/freecad_impl/shapes.py index 01850ab..03e34b5 100644 --- a/CadQuery/Libs/cadquery/freecad_impl/shapes.py +++ b/CadQuery/Libs/cadquery/freecad_impl/shapes.py @@ -616,7 +616,7 @@ class Solid(Shape): # needs to allow free-space wires ( those not made from a workplane ) @classmethod - def makeLoft(cls, listOfWire): + def makeLoft(cls, listOfWire, ruled=False): """ makes a loft from a list of wires The wires will be converted into faces when possible-- it is presumed that nobody ever actually @@ -624,7 +624,7 @@ class Solid(Shape): """ # the True flag requests building a solid instead of a shell. - return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True)) + return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True, ruled)) @classmethod def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None): diff --git a/CadQuery/Libs/cadquery/selectors.py b/CadQuery/Libs/cadquery/selectors.py index 42f0a24..72362c8 100644 --- a/CadQuery/Libs/cadquery/selectors.py +++ b/CadQuery/Libs/cadquery/selectors.py @@ -62,7 +62,7 @@ class NearestToPointSelector(Selector): def filter(self,objectList): def dist(tShape): - return tShape.Center().sub(self.pnt).Length + return tShape.Center().sub(Vector(*self.pnt)).Length #if tShape.ShapeType == 'Vertex': # return tShape.Point.sub(toVector(self.pnt)).Length #else: @@ -70,6 +70,49 @@ class NearestToPointSelector(Selector): return [ min(objectList,key=dist) ] +class BoxSelector(Selector): + """ + Selects objects inside the 3D box defined by 2 points. + + If `boundingbox` is True only the objects that have their bounding + box inside the given box is selected. Otherwise only center point + of the object is tested. + + Applicability: all types of shapes + + Example:: + + CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1)) + """ + def __init__(self, point0, point1, boundingbox=False): + self.p0 = Vector(*point0) + self.p1 = Vector(*point1) + self.test_boundingbox = boundingbox + + def filter(self, objectList): + + result = [] + x0, y0, z0 = self.p0.toTuple() + x1, y1, z1 = self.p1.toTuple() + + def isInsideBox(p): + # using XOR for checking if x/y/z is in between regardless + # of order of x/y/z0 and x/y/z1 + return ((p.x < x0) ^ (p.x < x1)) and \ + ((p.y < y0) ^ (p.y < y1)) and \ + ((p.z < z0) ^ (p.z < z1)) + + for o in objectList: + if self.test_boundingbox: + bb = o.BoundingBox() + if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \ + isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)): + result.append(o) + else: + if isInsideBox(o.Center()): + result.append(o) + + return result class BaseDirSelector(Selector): """