diff --git a/CadQuery/Libs/cadquery b/CadQuery/Libs/cadquery
deleted file mode 160000
index 40efd25..0000000
--- a/CadQuery/Libs/cadquery
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 40efd2599a867d633037fe3d47fe8299325f7b50
diff --git a/CadQuery/Libs/cadquery/CQ.py b/CadQuery/Libs/cadquery/CQ.py
new file mode 100644
index 0000000..ec0b583
--- /dev/null
+++ b/CadQuery/Libs/cadquery/CQ.py
@@ -0,0 +1,2255 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+"""
+
+import time,math
+from cadquery import *
+from cadquery import selectors
+
+class CQContext(object):
+ """
+ A shared context for modeling.
+
+ All objects in the same CQ chain share a reference to this same object instance
+ which allows for shared state when needed,
+ """
+ def __init__(self):
+ self.pendingWires = [] #a list of wires that have been created and need to be extruded
+ self.pendingEdges = [] #a list of pending edges that have been created and need to be joined into wires
+ self.firstPoint = None #a reference to the first point for a set of edges. used to determine how to behave when close() is called
+ self.tolerance = 0.0001 #user specified tolerance
+
+class CQ(object):
+ """
+ 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
+ """
+
+ def __init__(self,obj):
+ """
+ Construct a new cadquery (CQ) object that wraps a CAD primitive.
+
+ :param obj: Object to Wrap.
+ :type obj: A CAD Primitive ( wire,vertex,face,solid,edge )
+ """
+ self.objects = []
+ self.ctx = CQContext()
+ self.parent = None
+
+ if obj: #guarded because sometimes None for internal use
+ self.objects.append(obj)
+
+ def newObject(self,objlist):
+ """
+ Make a new CQ object.
+
+ :param objlist: The stack of objects to use
+ :param newContextSolid: an optional new solid to become the new context solid
+
+ :type objlist: a list of CAD primitives ( wire,face,edge,solid,vertex,etc )
+
+ The parent of the new object will be set to the current object,
+ to preserve the chain correctly.
+
+ Custom plugins and subclasses should use this method to create new CQ objects
+ correctly.
+ """
+ r = CQ(None) #create a completely blank one
+ r.parent = self
+ r.ctx = self.ctx #context solid remains the same
+ r.objects = list(objlist)
+ return r
+
+ def _collectProperty(self,propName):
+ """
+ Collects all of the values for propName,
+ for all items on the stack.
+ FreeCAD objects do not implement id correclty,
+ so hashCode is used to ensure we dont add the same
+ object multiple times.
+
+ One weird use case is that the stack could have a solid reference object
+ on it. This is meant to be a reference to the most recently modified version
+ of the context solid, whatever it is.
+ """
+ all = {}
+ for o in self.objects:
+
+ #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')():
+ all[i.hashCode()] = i
+ else:
+ 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):
+ """
+ Splits a solid on the stack into two parts, optionally keeping the separate parts.
+
+ :param boolean keepTop: True to keep the top, False or None to discard it
+ :param boolean keepBottom: True to keep the bottom, False or None to discard it
+ :raises: ValueError if keepTop and keepBottom are both false.
+ :raises: ValueError if there is not a solid in the current stack or the parent chain
+ :returns: CQ object with the desired objects on the stack.
+
+ The most common operation splits a solid and keeps one half. This sample creates split bushing::
+
+ #drill a hole in the side
+ c = Workplane().box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()F
+ #now cut it in half sideways
+ c.faces(">Y").workplane(-0.5).split(keepTop=True)
+
+ """
+
+ solid = self.findSolid()
+
+ if (not keepTop) and (not keepBottom):
+ raise ValueError("You have to keep at least one half")
+
+ maxDim = solid.BoundingBox().DiagonalLength * 10.0
+ topCutBox = self.rect(maxDim,maxDim)._extrude(maxDim)
+ bottomCutBox = self.rect(maxDim,maxDim)._extrude(-maxDim)
+
+ top = solid.cut(bottomCutBox)
+ bottom = solid.cut(topCutBox)
+
+ if keepTop and keepBottom:
+ #put both on the stack, leave original unchanged
+ return self.newObject([top,bottom])
+ else:
+ # put the one we are keeping on the stack, and also update the context solid
+ #to the one we kept
+ if keepTop:
+ solid.wrapped = top.wrapped
+ return self.newObject([top])
+ else:
+ solid.wrapped = bottom.wrapped
+ return self.newObject([bottom])
+
+
+ def combineSolids(self,otherCQToCombine=None):
+ """
+ !!!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.
+
+ :param otherCQToCombine: another cadquery to combine.
+ :return: a cQ object with the resulting combined solid on the stack.
+
+ Most of the time, both objects will contain a single solid, which is
+ combined and returned on the stack of the new object.
+
+ """
+ #loop through current stack objects, and combine them
+ #TODO: combine other types of objects as well, like edges and wires
+ toCombine = self.solids().vals()
+
+ if otherCQToCombine:
+ for obj in otherCQToCombine.solids().vals():
+ toCombine.append(obj)
+
+ if len(toCombine) < 1:
+ raise ValueError("Cannot Combine: at least one solid required!")
+
+ #get context solid
+ ctxSolid = self.findSolid(searchStack=False,searchParents=True) #we dont want to find our own objects
+
+ if ctxSolid is None:
+ ctxSolid = toCombine.pop(0)
+
+ #now combine them all. make sure to save a reference to the ctxSolid pointer!
+ s = ctxSolid
+ for tc in toCombine:
+ s = s.fuse(tc)
+
+ ctxSolid.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def all(self):
+ """
+ Return a list of all CQ objects on the stack.
+
+ useful when you need to operate on the elements
+ individually.
+
+ Contrast with vals, which returns the underlying
+ objects for all of the items on the stack
+
+ """
+ return [self.newObject([o]) for o in self.objects]
+
+ def size(self):
+ """
+ Return the number of objects currently on the stack
+
+ """
+ return len(self.objects)
+
+ def vals(self):
+ """
+ get the values in the current list
+
+ :rtype: list of FreeCAD objects
+ :returns: the values of the objects on the stack.
+
+ Contrast with :py:meth:`all`, which returns CQ objects for all of the items on the stack
+
+ """
+ res = []
+ return self.objects
+
+ def add(self,obj):
+ """
+ adds an object or a list of objects to the stack
+
+
+ :param obj: an object to add
+ :type obj: a CQ object, CAD primitive, or list of CAD primitives
+ :return: a CQ object with the requested operation performed
+
+ If an CQ object, the values of that object's stack are added. If a list of cad primitives,
+ they are all added. If a single CAD primitive it is added
+
+ Used in rare cases when you need to combine the results of several CQ results
+ into a single CQ object. Shelling is one common example
+
+ """
+ if type(obj) == list:
+ self.objects.extend(obj)
+ elif type(obj) == CQ or type(obj) == Workplane:
+ self.objects.extend(obj.objects)
+ else:
+ self.objects.append(obj)
+ return self
+
+ def val(self):
+ """
+ Return the first value on the stack
+
+ :return: the first value on the stack.
+ :rtype: A FreeCAD object or a SolidReference
+ """
+ return self.objects[0]
+
+ def 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.
+ :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):
+ """
+
+ Creates a new 2-D workplane, located relative to the first face on the stack.
+
+ :param offset: offset for the work plane in the Z direction. Default
+ :param invert: invert the Z direction from that of the face.
+ :type offset: float or None=0.0
+ :type invert: boolean or None=False
+ :rtype: Workplane object ( which is a subclass of CQ )
+
+ The first element on the stack must be a face, or a vertex. If a vertex, then the parent item on the
+ chain immediately before the vertex must be a face.
+
+ The result will be a 2-d working plane
+ with a new coordinate system set up as follows:
+
+ * The origin will be located in the *center* of the face, if a face was selected. If a vertex was
+ selected, the origin will be at the vertex, and located on the face.
+ * The Z direction will be normal to the plane of the face,computed
+ at the center point.
+ * The X direction will be parallel to the x-y plane. If the workplane is parallel to the global
+ x-y plane, the x direction of the workplane will co-incide with the global x direction.
+
+ Most commonly, the selected face will be planar, and the workplane lies in the same plane
+ of the face ( IE, offset=0). Occasionally, it is useful to define a face offset from
+ an existing surface, and even more rarely to define a workplane based on a face that is not planar.
+
+ To create a workplane without first having a face, use the Workplane() method.
+
+ Future Enhancements:
+ * Allow creating workplane from planar wires
+ * Allow creating workplane based on an arbitrary point on a face, not just the center.
+ For now you can work around by creating a workplane and then offsetting the center afterwards.
+
+ """
+ obj = self.objects[0]
+
+ def _computeXdir(normal):
+ xd = Vector(0,0,1).cross(normal)
+ if xd.Length < self.ctx.tolerance:
+ #this face is parallel with the x-y plane, so choose x to be in global coordinates
+ xd = Vector(1,0,0)
+ return xd
+
+ faceToBuildOn = None
+ center = None
+ #if isinstance(obj,Vertex):
+ # f = self.parent.objects[0]
+ # if f != None and isinstance(f,Face):
+ # center = obj.Center()
+ # normal = f.normalAt(center)
+ # xDir = _computeXdir(normal)
+ # else:
+ # raise ValueError("If a vertex is selected, a face must be the immediate parent")
+ if isinstance(obj,Face):
+ faceToBuildOn = obj
+ center = obj.Center()
+ normal = obj.normalAt(center)
+ xDir = _computeXdir(normal)
+ else:
+ if hasattr(obj,'Center'):
+ center = obj.Center()
+ normal = self.plane.zDir
+ xDir = self.plane.xDir
+ else:
+ raise ValueError ("Needs a face or a vertex or point on a work plane")
+
+ #invert if requested
+ if invert:
+ normal = normal.multiply(-1.0)
+
+ #offset origin if desired
+ offsetVector = normal.normalize().multiply(offset)
+ offsetCenter = center.add(offsetVector)
+
+ #make the new workplane
+ plane = Plane(offsetCenter, xDir, normal)
+ s = Workplane(plane)
+ s.parent = self
+ s.ctx = self.ctx
+
+ #a new workplane has the center of the workplane on the stack
+ return s
+
+ def first(self):
+ """
+ Return the first item on the stack
+ :returns: the first item on the stack.
+ :rtype: a CQ object
+ """
+ return self.newObject(self.objects[0:1])
+
+ def item(self,i):
+ """
+
+ Return the ith item on the stack.
+ :rtype: a CQ object
+ """
+ return self.newObject([self.objects[i]])
+
+ def last(self):
+ """
+ Return the last item on the stack.
+ :rtype: a CQ object
+ """
+ return self.newObject([self.objects[-1]])
+
+ def end(self):
+ """
+ Return the parent of this CQ element
+ :rtype: a CQ object
+ :raises: ValueError if there are no more parents in the chain.
+
+ For example::
+
+ CQ(obj).faces("+Z").vertices().end()
+
+ will return the same as::
+
+ CQ(obj).faces("+Z")
+
+ """
+ if self.parent:
+ return self.parent
+ else:
+ raise ValueError("Cannot End the chain-- no parents!")
+
+
+
+ def findSolid(self,searchStack=True,searchParents=True):
+ """
+ Finds the first solid object in the chain, searching from the current node
+ backwards through parents until one is found.
+
+ :param searchStack: should objects on the stack be searched first.
+ :param searchParents: should parents be searched?
+ :raises: ValueError if no solid is found in the current object or its parents, and errorOnEmpty is True
+
+ This function is very important for chains that are modifying a single parent object, most often
+ a solid.
+
+ Most of the time, a chain defines or selects a solid, and then modifies it using workplanes
+ or other operations.
+
+ Plugin Developers should make use of this method to find the solid that should be modified, if the
+ plugin implements a unary operation, or if the operation will automatically merge its results with an
+ object already on the stack.
+ """
+ #notfound = ValueError("Cannot find a Valid Solid to Operate on!")
+
+ if searchStack:
+ for s in self.objects:
+ if type(s) == Solid:
+ return s
+
+ if searchParents and self.parent is not None:
+ return self.parent.findSolid(searchStack=True,searchParents=searchParents)
+
+ return None
+
+ def _selectObjects(self,objType,selector=None):
+ """
+ Filters objects of the selected type with the specified selector,and returns results
+
+ :param objType: the type of object we are searching for
+ :type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid)
+ :return: a CQ object with the selected objects on the stack.
+
+ **Implementation Note**: This is the base implmentation of the vertices,edges,faces,solids,shells,
+ and other similar selector methods. It is a useful extension point for plugin developers to make
+ other selector methods.
+ """
+ toReturn = self._collectProperty(objType) #all of the faces from all objects on the stack, in a single list
+
+ if selector is not None:
+ if type(selector) == str:
+ selectorObj = selectors.StringSyntaxSelector(selector)
+ else:
+ selectorObj = selector
+ toReturn = selectorObj.filter(toReturn)
+
+ return self.newObject(toReturn)
+
+ 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.
+
+ :param selector:
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object whos stack contains the *distinct* vertices of *all* objects on the current stack,
+ after being filtered by the selector, if provided
+
+ If there are no vertices for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the vertices of a single object on the stack. For example::
+
+ Workplane().box(1,1,1).faces("+Z").vertices().size()
+
+ returns 4, because the topmost face of cube will contain four vertices. While this::
+
+ Workplane().box(1,1,1).faces().vertices().size()
+
+ returns 8, because a cube has a total of 8 vertices
+
+ **Note** Circles are peculiar, they have a single vertex at the center!
+
+ :py:class:`StringSyntaxSelector`
+
+ """
+ return self._selectObjects('Vertices',selector)
+
+ def faces(self,selector=None):
+ """
+ Select the faces of objects on the stack, optionally filtering the selection. If there are multiple objects
+ on the stack, the faces of all objects are collected and a list of all the distinct faces is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object whos stack contains all of the *distinct* faces of *all* objects on the current stack,
+ filtered by the provided selector.
+
+ If there are no vertices for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the faces of a single object on the stack. For example::
+
+ CQ(aCube).faces("+Z").size()
+
+ returns 1, because a cube has one face with a normal in the +Z direction. Similarly::
+
+ CQ(aCube).faces().size()
+
+ returns 6, because a cube has a total of 6 faces, And::
+
+ CQ(aCube).faces("|Z").size()
+
+ returns 2, because a cube has 2 faces having normals parallel to the z direction
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Faces',selector)
+
+ def edges(self,selector=None):
+ """
+ Select the edges of objects on the stack, optionally filtering the selection. If there are multiple objects
+ on the stack, the edges of all objects are collected and a list of all the distinct edges is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object whos stack contains all of the *distinct* edges of *all* objects on the current stack,
+ filtered by the provided selector.
+
+ If there are no edges for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the edges of a single object on the stack. For example::
+
+ CQ(aCube).faces("+Z").edges().size()
+
+ returns 4, because a cube has one face with a normal in the +Z direction. Similarly::
+
+ CQ(aCube).edges().size()
+
+ returns 12, because a cube has a total of 12 edges, And::
+
+ CQ(aCube).edges("|Z").size()
+
+ returns 4, because a cube has 4 edges parallel to the z direction
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Edges',selector)
+
+ def wires(self,selector=None):
+ """
+ Select the wires of objects on the stack, optionally filtering the selection. If there are multiple objects
+ on the stack, the wires of all objects are collected and a list of all the distinct wires is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object whos stack contains all of the *distinct* wires of *all* objects on the current stack,
+ filtered by the provided selector.
+
+ If there are no wires for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the wires of a single object on the stack. For example::
+
+ CQ(aCube).faces("+Z").wires().size()
+
+ returns 1, because a face typically only has one outer wire
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Wires',selector)
+
+ def solids(self,selector=None):
+ """
+ Select the solids of objects on the stack, optionally filtering the selection. If there are multiple objects
+ on the stack, the solids of all objects are collected and a list of all the distinct solids is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack,
+ filtered by the provided selector.
+
+ If there are no solids for any objects on the current stack, an empty CQ object is returned
+
+ The typical use is to select the a single object on the stack. For example::
+
+ CQ(aCube).solids().size()
+
+ returns 1, because a cube consists of one solid.
+
+ It is possible for single CQ object ( or even a single CAD primitive ) to contain multiple solids.
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Solids',selector)
+
+ def shells(self,selector=None):
+ """
+ Select the shells of objects on the stack, optionally filtering the selection. If there are multiple objects
+ on the stack, the shells of all objects are collected and a list of all the distinct shells is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack,
+ filtered by the provided selector.
+
+ If there are no shells for any objects on the current stack, an empty CQ object is returned
+
+ Most solids will have a single shell, which represents the outer surface. A shell will typically be
+ composed of multiple faces.
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Shells',selector)
+
+ def compounds(self,selector=None):
+ """
+ Select compounds on the stack, optionally filtering the selection. If there are multiple objects
+ on the stack, they are collected and a list of all the distinct compounds is returned.
+
+ :param selector: A selector
+ :type selector: None, a Selector object, or a string selector expression.
+ :return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack,
+ filtered by the provided selector.
+
+ A compound contains multiple CAD primitives that resulted from a single operation, such as a union, cut,
+ split, or fillet. Compounds can contain multiple edges, wires, or solids.
+
+ See more about selectors HERE
+ """
+ return self._selectObjects('Compounds',selector)
+
+ def toSvg(self,opts=None):
+ """
+ Returns svg text that represents the first item on the stack.
+
+ for testing purposes.
+
+ :param options: svg formatting options
+ :type options: dictionary, width and height
+ :return: a string that contains SVG that represents this item.
+ """
+ return SVGexporter.getSVG(self.val().wrapped,opts)
+
+ def exportSvg(self,fileName):
+ """
+ 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
+
+ """
+ exporters.exportSVG(self,fileName)
+
+ def rotateAboutCenter(self,axisEndPoint,angleDegrees):
+ """
+ Rotates all items on the stack by the specified angle, about the specified axis
+
+ The center of rotation is a vector starting at the center of the object on the stack,
+ and ended at the specified point.
+
+ :param axisEndPoint: the second point of axis of rotation
+ :type axisEndPoint: a three-tuple in global coordinates
+ :param angleDegrees: the rotation angle, in degrees
+ :type angleDegrees: float
+ :returns: a CQ object, with all items rotated.
+
+ WARNING: This version returns the same cq object instead of a new one-- the
+ old object is not accessible.
+
+ Future Enhancements:
+ * A version of this method that returns a transformed copy, rather than modifying
+ the originals
+ * This method doesnt expose a very good interface, becaues the axis of rotation
+ could be inconsistent between multiple objects. This is because the beginning
+ of the axis is variable, while the end is fixed. This is fine when operating on
+ one object, but is not cool for multiple.
+
+ """
+
+ #center point is the first point in the vector
+ endVec = Vector(axisEndPoint)
+
+ def _rot(obj):
+ startPt = obj.Center()
+ endPt = startPt + endVec
+ obj.rotate(startPt,endPt,angleDegrees)
+
+ return self.each(_rot,False)
+
+ def translate(self,vec):
+ """
+ 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.
+ """
+ return self.newObject([o.translate(vec) for o in self.objects])
+
+
+ def shell(self,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.
+
+ :param thickness: a positive float, representing the thickness of the desired shell. Negative values shell inwards,
+ positive values shell outwards.
+ :raises: ValueError if the current stack contains objects that are not faces of a solid further
+ up in the chain.
+ :returns: a CQ object with the resulting shelled solid selected.
+
+ This example will create a hollowed out unit cube, where the top most face is open,
+ and all other walls are 0.2 units thick::
+
+ Workplane().box(1,1,1).faces("+Z").shell(0.2)
+
+ Shelling is one of the cases where you may need to use the add method to select several faces. For
+ example, this example creates a 3-walled corner, by removing three faces of a cube::
+
+ s = Workplane().box(1,1,1)
+ s1 = s.faces("+Z")
+ s1.add(s.faces("+Y")).add(s.faces("+X"))
+ self.saveModel(s1.shell(0.2))
+
+ This fairly yucky syntax for selecting multiple faces is planned for improvement
+
+ **Note**: When sharp edges are shelled inwards, they remain sharp corners, but **outward** shells are
+ automatically filleted, because an outward offset from a corner generates a radius
+
+
+ Future Enhancements:
+ Better selectors to make it easier to select multiple faces
+
+ """
+ solidRef = self.findSolid()
+
+ for f in self.objects:
+ if type(f) != Face:
+ raise ValueError ("Shelling requires that faces be selected")
+
+ s = solidRef.shell(self.objects,thickness)
+ solidRef.wrapped = s.wrapped
+ return self.newObject([s])
+
+
+ def fillet(self,radius):
+ """
+ Fillets a solid on the selected edges.
+
+ The edges on the stack are filleted. The solid to which the edges belong must be in the parent chain
+ of the selected edges.
+
+ :param radius: the radius of the fillet, must be > zero
+ :type radius: positive float
+ :raises: ValueError if at least one edge is not selected
+ :raises: ValueError if the solid containing the edge is not in the chain
+ :returns: cq object with the resulting solid selected.
+
+ This example will create a unit cube, with the top edges filleted::
+
+ s = Workplane().box(1,1,1).faces("+Z").edges().fillet(0.1)
+ """
+ #TODO: we will need much better edge selectors for this to work
+ #TODO: ensure that edges selected actually belong to the solid in the chain, otherwise, fe segfault
+
+ solid = self.findSolid()
+
+ edgeList = self.edges().vals()
+ if len(edgeList) < 1:
+ raise ValueError ("Fillets requires that edges be selected")
+
+ s = solid.fillet(radius,edgeList)
+ solid.wrapped = s.wrapped
+ return self.newObject([s])
+
+class Workplane(CQ):
+ """
+ Defines a coordinate system in space, in which 2-d coordinates can be used.
+
+ :param plane: the plane in which the workplane will be done
+ :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
+ :raises: ValueError if the provided plane is not a plane, a valid named workplane
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane, and the
+ *current point* is on the origin.
+
+ .. note::
+ You can also create workplanes on the surface of existing faces using
+ :py:meth:`CQ.workplane`
+
+
+ """
+
+ FOR_CONSTRUCTION = 'ForConstruction'
+
+
+ def __init__(self, inPlane ,origin=(0,0,0), obj=None):
+ """
+ make a workplane from a particular plane
+
+ :param plane: the plane in which the workplane will be done
+ :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
+ :raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane, and the
+ *current point* is on the origin.
+ """
+
+ if inPlane.__class__.__name__ == 'Plane':
+ tmpPlane = inPlane
+ elif type(inPlane) == str:
+ tmpPlane = Plane.named(inPlane,origin)
+ else:
+ tmpPlane = None
+
+ if tmpPlane == None:
+ raise ValueError(" Provided value %s is not a valid work plane." % str(inPlane))
+
+ self.obj = obj
+ self.plane = tmpPlane
+ self.firstPoint = None
+ self.objects = [self.plane.origin] #changed so that workplane has the center as the first item on the stack
+ self.parent = None
+ self.ctx = CQContext()
+
+ def transformed(self,rotate=(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
+ :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
+ """
+
+ #old api accepted a vector, so we'll check for that.
+ if rotate.__class__.__name__ == 'Vector':
+ rotate = rotate.toTuple()
+
+ if offset.__class__.__name__ == 'Vector':
+ offset = offset.toTuple()
+
+ p = self.plane.rotated(rotate)
+ p.setOrigin3d(self.plane.toWorldCoords(offset ))
+ ns = self.newObject([p.origin])
+ ns.plane = p
+
+ return ns
+
+ def newObject(self,objlist):
+ """
+ Create a new workplane object from this one.
+
+ Overrides CQ.newObject, and should be used by extensions, plugins, and
+ subclasses to create new objects.
+
+ :param objlist: new objects to put on the stack
+ :type objlist: a list of CAD primitives
+ :return: a new Workplane object with the current workplane as a parent.
+
+ """
+
+ #copy the current state to the new object
+ ns = Workplane("XY")
+ ns.plane = self.plane
+ ns.parent = self
+ ns.objects = list(objlist)
+ ns.ctx = self.ctx
+ return ns
+
+ def _findFromPoint(self,useLocalCoords=False):
+ """
+ finds the start point for an operation when an existing point
+ is implied. Examples include 2d operations such as lineTo,
+ which allows specifying the end point, and implicitly use the
+ end of the previous line as the starting point
+
+ :return: a Vector representing the point to use, or none if
+ such a point is not available.
+
+ :param useLocalCoords: selects whether the point is returned
+ in local coordinates or global coordinates.
+
+ The algorithm is this:
+ * If an Edge is on the stack, its end point is used.yp
+ * if a vector is on the stack, it is used
+
+ WARNING: only the first object on the stack is used.
+
+ NOTE:
+ """
+ obj = self.objects[0]
+ p = None
+ if isinstance(obj,Edge):
+ p = obj.endPoint()
+ elif isinstance(obj,Vector):
+ p = obj
+ else:
+ raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj))
+
+ if useLocalCoords:
+ return self.plane.toLocalCoords(p)
+ else:
+ return p
+
+ def 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
+
+ :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
+ """
+
+ 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
+ for x in range(xCount):
+ for y in range(yCount):
+ lpoints.append( (xSpacing*(x), ySpacing*(y)) )
+
+ #shift points down and left relative to origin if requested
+ if center:
+ xc = xSpacing*(xCount-1) * 0.5
+ yc = ySpacing*(yCount-1) * 0.5
+ cpoints = []
+ for p in lpoints:
+ cpoints.append( ( p[0] - xc, p[1] - yc ))
+ lpoints = list(cpoints)
+
+ return self.pushPoints(lpoints)
+
+ 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
+
+ :param pntList: a list of points to push onto the stack
+ :type pntList: list of 2-tuples, in *local* coordinates
+ :return: a new workplane with the desired points on the stack.
+
+ A common use is to provide a list of points for a subsequent operation, such as creating circles or holes.
+ This example creates a cube, and then drills three holes through it, based on three points::
+
+ s = Workplane().box(1,1,1).faces(">Z").workplane().pushPoints([(-0.3,0.3),(0.3,0.3),(0,0)])
+ body = s.circle(0.05).cutThruAll()
+
+ Here the circle function operates on all three points, and is then extruded to create three holes.
+ See :py:meth:`circle` for how it works.
+
+ """
+ vecs = []
+ for pnt in pntList:
+ vec = self.plane.toWorldCoords(pnt)
+ vecs.append(vec)
+
+ return self.newObject(vecs)
+
+ def center(self,x,y):
+ """
+ Shift local coordinates to the specified location.
+
+ The location is specified in terms of local coordinates.
+
+ :param float x: the new x location
+ :param float y: the new y location
+ :returns: the workplane object, with the center adjusted.
+
+ The current point is set to the new center.
+ This method is useful to adjust the center point after it has been created automatically on a face,
+ but not where you'd like it to be.
+
+ In this example, we adjust the workplane center to be at the corner of a cube, instead of
+ the center of a face, which is the default::
+
+ #this workplane is centered at x=0.5,y=0.5, the center of the upper face
+ s = Workplane().box(1,1,1).faces(">Z").workplane()
+
+ s.center(-0.5,-0.5) # move the center to the corner
+ t = s.circle(0.25).extrude(0.2)
+ assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner
+
+ The result is a cube with a round boss on the corner
+
+ """
+ "Shift local coordinates to the specified location, according to current coordinates"
+ self.plane.setOrigin2d(x,y)
+ n = self.newObject([self.plane.origin])
+ return n
+
+ def lineTo(self, x, y,forConstruction=False):
+ """
+ Make a line from the current point to the provided point
+
+ :param float x: the x point, in workplane plane coordinates
+ :param float y: the y point, in workplane plane coordinates
+ :return: the Workplane object with the current point at the end of the new line
+
+ see :py:meth:`line` if you want to use relative dimensions to make a line instead.
+
+ """
+ startPoint = self._findFromPoint(False)
+
+ endPoint = self.plane.toWorldCoords((x, y))
+
+ p = Edge.makeLine(startPoint,endPoint)
+
+ if not forConstruction:
+ self._addPendingEdge(p)
+
+ return self.newObject([p])
+
+ #line a specified incremental amount from current point
+ def line(self, xDist, yDist ,forConstruction=False):
+ """
+ Make a line from the current point to the provided point, using
+ dimensions relative to the current point
+
+ :param float xDist: x distance from current point
+ :param float yDist: y distance from current point
+ :return: the workplane object with the current point at the end of the new line
+
+ see :py:meth:`lineTo` if you want to use absolute coordinates to make a line instead.
+
+ """
+ p = self._findFromPoint(True) #return local coordinates
+ return self.lineTo(p.x + xDist, yDist + p.y,forConstruction)
+
+ def vLine(self, distance,forConstruction=False):
+ """
+ Make a vertical line from the current point the provided distance
+
+ :param float distance: (y) distance from current point
+ :return: the workplane object with the current point at the end of the new line
+
+ """
+ return self.line(0, distance,forConstruction)
+
+ def vLineTo(self,yCoord,forConstruction=False):
+ """
+ Make a vertcial line from the current point to the provided y coordinate.
+
+ Useful if it is more convienient to specify the end location rather than distance,
+ as in :py:meth:`vLine`
+
+ :param float yCoord: y coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(p.x,yCoord,forConstruction)
+
+ def hLineTo(self,xCoord,forConstruction=False):
+ """
+ Make a horizontal line from the curren tpoint to the provided x coordinate.
+
+ Useful if it is more convienient to specify the end location rather than distance,
+ as in :py:meth:`hLine`
+
+ :param float xCoord: x coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(xCoord,p.y,forConstruction)
+
+ def hLine(self, distance,forConstruction=False):
+ """
+ Make a horizontal line from the current point the provided distance
+
+ :param float distance: (x) distance from current point
+ :return: the Workplane object with the current point at the end of the new line
+
+ """
+ return self.line(distance, 0,forConstruction)
+
+ #absolute move in current plane, not drawing
+ def moveTo(self, x=0, y=0):
+ """
+ Move to the specified point, without drawing.
+
+ :param x: desired x location, in local coordinates
+ :type x: float, or none for zero
+ :param y: desired y location, in local coorindates
+ :type y: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`move` to do the same thing but using relative dimensions
+ """
+ newCenter = Vector(x,y,0)
+ return self.newObject([self.plane.toWorldCoordinates(newCenter)])
+
+ #relative move in current plane, not drawing
+ def move(self, xDist=0, yDist=0):
+ """
+ Move the specified distance from the current point, without drawing.
+
+ :param xDist: desired x distance, in local coordinates
+ :type xDist: float, or none for zero
+ :param yDist: desired y distance, in local coorindates
+ :type yDist: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`moveTo` to do the same thing but using absolute coordinates
+ """
+ p = self._findFromPoint(True)
+ newCenter = p + Vector(xDist,yDist,0)
+ return self.newObject([self.plane.toWorldCoordinates(newCenter)])
+
+
+ def spline(self,listOfXYTuple,forConstruction=False):
+ """
+ Create a spline interpolated through the provided points.
+
+ :param listOfXYTuple: points to interpolate through
+ :type listOfXYTuple: list of 2-tuple
+ :return: a Workplane object with the current point at the end of the spline
+
+ The spline will begin at the current point, and
+ end with the last point in the XY typle list
+
+ This example creates a block with a spline for one side::
+
+ s = Workplane(Plane.XY())
+ sPnts = [
+ (2.75,1.5),
+ (2.5,1.75),
+ (2.0,1.5),
+ (1.5,1.0),
+ (1.0,1.25),
+ (0.5,1.0),
+ (0,1.0)
+ ]
+ r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close()
+ r = r.extrude(0.5)
+
+ *WARNING* It is fairly easy to create a list of points
+ that cannot be correctly interpreted as a spline.
+
+ Future Enhancements:
+ * provide access to control points
+
+ """
+ gstartPoint = self._findFromPoint(False)
+ gEndPoint = self.plane.toWorldCoords(listOfXYTuple[-1])
+
+ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
+ allPoints = [gstartPoint] + vecs
+
+ e = Edge.makeSpline(allPoints)
+
+ if not forConstruction:
+ self._addPendingEdge(e)
+
+ return self.newObject([e])
+
+ def threePointArc(self,point1, point2,forConstruction=False):
+ """
+ Draw an arc from the current point, through point1, and ending at point2
+
+ :param point1: point to draw through
+ :type point1: 2-tuple, in workplane coordinates
+ :param point2: end point for the arc
+ :type point2: 2-tuple, in workplane coordinates
+ :return: a workplane with the current point at the end of the arc
+
+ Future Enhancments:
+ provide a version that allows an arc using relative measures
+ provide a centerpoint arc
+ provide tangent arcs
+
+ """
+
+ gstartPoint = self._findFromPoint(False)
+ gpoint1 = self.plane.toWorldCoords(point1)
+ gpoint2 = self.plane.toWorldCoords(point2)
+
+ arc = Edge.makeThreePointArc(gstartPoint,gpoint1,gpoint2)
+
+ if not forConstruction:
+ self._addPendingEdge(arc)
+
+ return self.newObject([arc])
+
+ def rotateAndCopy(self,matrix):
+ """
+ Makes a copy of all edges on the stack, rotates them according to the
+ provided matrix, and then attempts to consolidate them into a single wire.
+
+ :param matrix: a 4xr transformation matrix, in global coordinates
+ :type matrix: a FreeCAD Base.Matrix object
+ :return: a cadquery object with consolidated wires, and any originals on the stack.
+
+ The most common use case is to create a set of open edges, and then mirror them
+ around either the X or Y axis to complete a closed shape.
+
+ see :py:meth:`mirrorX` and :py:meth:`mirrorY` to mirror about the global X and Y axes
+ see :py:meth:`mirrorX` and for an example
+
+ Future Enhancements:
+ faster implementation: this one transforms 3 times to accomplish the result
+
+
+ """
+
+ #convert edges to a wire, if there are pending edges
+ n = self.wire(forConstruction=False)
+
+ #attempt to consolidate wires together.
+ consolidated = n.consolidateWires()
+
+ rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(),matrix)
+
+ for w in rotatedWires:
+ consolidated.objects.append(w)
+ consolidated._addPendingWire(w)
+
+ #attempt again to consolidate all of the wires
+ c = consolidated.consolidateWires()
+ return c
+
+ def mirrorY(self):
+ """
+ Mirror entities around the y axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier. This line of code::
+
+ s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25)
+
+ Produces a flat, heart shaped object
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+
+ """
+ tm = Matrix()
+ tm.rotateY(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def mirrorX(self):
+ """
+ Mirror entities around the x axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier.
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+
+ """
+ tm = Matrix()
+ tm.rotateX(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def _addPendingEdge(self,edge):
+ """
+ Queues an edge for later combination into a wire.
+
+ :param edge:
+ :return:
+ """
+ self.ctx.pendingEdges.append(edge)
+
+ if self.ctx.firstPoint is None:
+ self.ctx.firstPoint = edge.startPoint()
+
+ def _addPendingWire(self,wire):
+ """
+ Queue a Wire for later extrusion
+
+ Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids.
+
+ but users do not normally care about these distinctions. Users 'think' in terms
+ of edges, and solids.
+
+ CadQuery tracks edges as they are drawn, and automatically combines them into wires
+ when the user does an operation that needs it.
+
+ Similarly, cadQuery tracks pending wires, and automaticlaly combines them into faces
+ when necessary to make a solid.
+ """
+ self.ctx.pendingWires.append(wire)
+
+
+ def consolidateWires(self):
+ """
+ Attempt to consolidate wires on the stack into a single.
+ If possible, a new object with the results are returned.
+ if not possible, the wires remain separated
+
+ FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly somtimes
+ Additionally, it has a bug where a profile compose of two wires ( rathre than one )
+ also does not work properly
+
+ together these are a real problem.
+ """
+ wires = self.wires().vals()
+ if len(wires) < 2:
+ return self
+
+ #TODO: this makes the assumption that either all wires could be combined, or none.
+ #in reality trying each combination of wires is probably not reasonable anyway
+ w = Wire.combine(wires)
+
+ #ok this is a little tricky. if we consolidate wires, we have to actually
+ #modify the pendingWires collection to remove the original ones, and replace them
+ #with the consolidate done
+ #since we are already assuming that all wires could be consolidated, its easy, we just
+ #clear the pending wire list
+ r = self.newObject([w])
+ r.ctx.pendingWires = []
+ r._addPendingWire(w)
+ return r
+
+
+
+ def wire(self,forConstruction=False):
+ """
+ Returns a CQ object with all pending edges connected into a wire.
+
+ All edges on the stack that can be combined will be combined into a single wire object,
+ and other objects will remain on the stack unmodified
+
+ :param forConstruction: whether the wire should be used to make a solid, or if it is just for reference
+ :type forConstruction: boolean. true if the object is only for reference
+
+ This method is primarily of use to plugin developers making utilites for 2-d construction. This method
+ shoudl be called when a user operation implies that 2-d construction is finished, and we are ready to
+ begin working in 3d
+
+ SEE '2-d construction concepts' for a more detailed explanation of how cadquery handles edges, wires, etc
+
+ Any non edges will still remain.
+ """
+
+ edges = self.ctx.pendingEdges
+
+ #do not consolidate if there are no free edges
+ if len(edges) == 0:
+ return self
+
+ self.ctx.pendingEdges = []
+
+ others = []
+ for e in self.objects:
+ if type(e) != Edge:
+ others.append(e)
+
+
+ w = Wire.assembleEdges(edges)
+ if not forConstruction:
+ self._addPendingWire(w)
+
+ return self.newObject(others + [w])
+
+ def each(self,callBackFunction,useLocalCoordinates=False):
+ """
+ runs the provided function on each value in the stack, and collects the return values into a new CQ
+ object.
+
+ Special note: a newly created workplane always has its center point as its only stack item
+
+ :param callBackFunction: the function to call for each item on the current stack.
+ :param useLocalCoordinates: should values be converted from local coordinates first?
+ :type useLocalCoordinates: boolean
+
+ The callback function must accept one argument, which is the item on the stack, and return
+ one object, which is collected. If the function returns None, nothing is added to the stack.
+ The object passed into the callBackFunction is potentially transformed to local coordinates, if
+ useLocalCoordinates is true
+
+ useLocalCoordinates is very useful for plugin developers.
+
+ If false, the callback function is assumed to be working in global coordinates. Objects created are added
+ as-is, and objects passed into the function are sent in using global coordinates
+
+ If true, the calling function is assumed to be working in local coordinates. Objects are transformed
+ to local coordinates before they are passed into the callback method, and result objects are transformed
+ to global coorindates after they are returned.
+
+ This allows plugin developers to create objects in local coordinates, without worrying
+ about the fact that the working plane is different than the global coordinate system.
+
+
+ TODO: wrapper object for Wire will clean up forConstruction flag everywhere
+
+ """
+ results = []
+ for obj in self.objects:
+
+ if useLocalCoordinates:
+ #TODO: this needs to work for all types of objects, not just vectors!
+ r = callBackFunction(self.plane.toLocalCoords(obj))
+ r = r.transformShape(self.plane.rG)
+ else:
+ r = callBackFunction(obj)
+
+
+ if type(r) == Wire:
+ if not r.forConstruction:
+ self._addPendingWire(r)
+
+ results.append ( r )
+
+
+ return self.newObject(results)
+
+ def eachpoint(self,callbackFunction, useLocalCoordinates=False):
+ """
+ Same as each(), except each item on the stack is converted into a point before it
+ is passed into the callback function.
+
+ :return: CadQuery object which contains a list of vectors (points ) on its stack.
+
+ :param useLocalCoordinates: should points be in local or global coordinates
+ :type useLocalCoordinates: boolean
+
+ The resulting object has a point on the stack for each object on the original stack.
+ Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted
+ to a point by using their center of mass.
+
+ If the stack has zero length, a single point is returned, which is the center of the current
+ workplane/coordinate system
+
+ """
+ #convert stack to a list of points
+ pnts = []
+ if len(self.objects) == 0:
+ #nothing on the stack. here, we'll assume we should operate with the
+ #origin as the context point
+ pnts.append(self.plane.origin)
+ else:
+
+ for v in self.objects:
+ pnts.append(v.Center())
+
+ return self.newObject(pnts).each(callbackFunction,useLocalCoordinates )
+
+
+ #make a rectangle
+ def rect(self,xLen,yLen,centered=True,forConstruction=False):
+ """
+ Make a rectangle for each item on the stack.
+
+ :param xLen: length in xDirection ( in workplane coordinates )
+ :type xLen: float > 0
+ :param yLen: length in yDirection ( in workplane coordinates )
+ :type yLen: float > 0
+ :param boolean centered: true if the rect is centered on the reference point, false if the lower-left is on the reference point
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating part geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+
+ """
+ def makeRectangleWire(pnt):
+ #here pnt is in local coordinates due to useLocalCoords=True
+ (xc,yc,zc) = pnt.toTuple()
+ if centered:
+ p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0,0) )
+ p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0,0) )
+ p3 = pnt.add(Vector(xLen/2.0, yLen/2.0,0) )
+ p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0,0) )
+ else:
+ p1 = pnt
+ p2 = pnt.add(Vector(xLen,0,0))
+ p3 = pnt.add(Vector( xLen,yLen,0 ))
+ p4 = pnt.add(Vector(0,yLen,0))
+
+ w = Wire.makePolygon([p1,p2,p3,p4,p1],forConstruction)
+ return w
+ #return Part.makePolygon([p1,p2,p3,p4,p1])
+
+ return self.eachpoint(makeRectangleWire,True)
+
+ #circle from current point
+ def circle(self,radius,forConstruction=False):
+ """
+ Make a circle for each item on the stack.
+
+ :param radius: radius of the circle
+ :type radius: float > 0
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating part geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin. Another common case is to use
+ successive circle() calls to create concentric circles. This works because the center of a circle
+ is its reference point::
+
+ s = Workplane().circle(2.0).circle(1.0)
+
+ Creates two concentric circles, which when extruded will form a ring.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+
+ """
+ def makeCircleWire(obj):
+ cir = Wire.makeCircle(radius,obj,Vector(0,0,1))
+ cir.forConstruction = forConstruction
+ return cir
+
+ return self.eachpoint(makeCircleWire,useLocalCoordinates=True)
+
+ def polygon(self,nSides,diameter):
+ """
+ Creates a polygon incribed in a circle of the specified diamter for each point on the stack
+
+ The first vertex is always oriented in the x direction.
+
+ :param nSides: number of sides, must be > 3
+ :param diameter: the size of the circle the polygon is incribed into
+ :return: a polygon wire
+
+
+ """
+ def _makePolygon(center):
+ #pnt is a vector in local coordinates
+ angle = 2.0 *math.pi / nSides
+ pnts = []
+ for i in range(nSides+1):
+ pnts.append( center + Vector((diameter / 2.0 * math.cos(angle*i)),(diameter / 2.0 * math.sin(angle*i)),0))
+ return Wire.makePolygon(pnts)
+
+ return self.eachpoint(_makePolygon,True)
+
+ def polyline(self,listOfXYTuple,forConstruction=False):
+ """
+ Create a polyline from a list of points
+
+ :param listOfXYTuple: a list of points in Workplane coordinates
+ :type listOfXYTuple: list of 2-tuples
+ :param forConstruction: should the new wire be reference geometry only?
+ :type forConstruction: true if the wire is for reference, false if they are creating part geometry
+ :return: a new CQ object with the new wire on the stack
+
+ *NOTE* most commonly, the resulting wire should be closed.
+
+ Future Enhacement:
+ This should probably yield a list of edges, not a wire, so that
+ it is possible to combine a polyline with other edges and arcs
+ """
+ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
+ w = Wire.makePolygon(vecs)
+ if not forConstruction:
+ self._addPendingWire(w)
+
+ return self.newObject([w])
+
+ #finish a set of lines.
+ #
+ def close(self):
+ """
+ End 2-d construction, and attempt to build a closed wire.
+
+ :return: a CQ object with a completed wire on the stack, if possible.
+
+ After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary
+ to convert the edges produced by these into one or more wires.
+
+ When a set of edges is closed, cadQuery assumes it is safe to build the group of edges
+ into a wire. This example builds a simple triangular prism::
+
+ s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2)
+
+ """
+ self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y)
+ return self.wire()
+
+ def largestDimension(self):
+ """
+ Finds the largest dimension in the stack.
+ Used internally to create thru features, this is how you can compute
+ how long or wide a feature must be to make sure to cut through all of the material
+ :return:
+ """
+ #TODO: this implementation is naive and returns the dims of the first solid... most of
+ #the time this works. but a stronger implementation would be to search all solids.
+ s = self.findSolid()
+ if s:
+ return s.BoundingBox().DiagonalLength * 5.0
+ else:
+ return 1000000
+
+ def cutEach(self,fcn,useLocalCoords=False):
+ """
+ Evaluates the provided function at each point on the stack ( ie, eachpoint )
+ and then cuts the result from the context solid.
+ :param function: a function suitable for use in the eachpoint method: ie, that accepts a vector
+ :param useLocalCoords: same as for :py:meth:`eachpoint`
+ :return: a CQ object that contains the resulting solid
+ :raises: an error if there is not a context solid to cut from
+ """
+ ctxSolid = self.findSolid()
+ if ctxSolid is None:
+ raise ValueError ("Must have a solid in the chain to cut from!")
+
+ #will contain all of the counterbores as a single compound
+ results = self.eachpoint(fcn,useLocalCoords).vals()
+ s = ctxSolid
+ for cb in results:
+ s = s.cut(cb)
+
+ ctxSolid.wrapped = s.wrapped
+ return self.newObject([s])
+
+ #but parameter list is different so a simple function pointer wont work
+ def cboreHole(self,diameter,cboreDiameter,cboreDepth,depth=None):
+ """
+ Makes a counterbored hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param cboreDiameter: the diameter of the cbore
+ :type cboreDiameter: float > 0 and > diameter
+ :param cboreDepth: depth of the counterbore
+ :type cboreDepth: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane plane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().cboreHole(0.125, 0.25,0.125,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. Counterbored holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cskHole` to make countersinks instead of counterbores
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCbore(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0,0,-1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
+
+ #add the counter bore
+ cbore = Solid.makeCylinder(cboreDiameter/2.0,cboreDepth,center,boreDir)
+ r = hole.fuse(cbore)
+ return r
+
+ return self.cutEach(_makeCbore,True)
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def cskHole(self,diameter, cskDiameter,cskAngle,depth=None):
+ """
+ Makes a countersunk hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param cskDiameter: the diameter of the countersink
+ :type cskDiameter: float > 0 and > diameter
+ :param cskAngle: angle of the countersink, in degrees ( 82 is common )
+ :type cskAngle: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().cskHole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` to make counterbores instead of countersinks
+ """
+
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCsk(center):
+ #center is in local coordinates
+
+ boreDir = Vector(0,0,-1)
+
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coords!
+ r = cskDiameter / 2.0
+ h = r / math.tan(math.radians(cskAngle / 2.0))
+ csk = Solid.makeCone(r,0.0,h,center,boreDir)
+ r = hole.fuse(csk)
+ return r
+
+ return self.cutEach(_makeCsk,True)
+
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def hole(self,diameter,depth=None):
+ """
+ Makes a hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().hole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` and :py:meth:`cskHole` to make counterbores or countersinks
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeHole(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0,0,-1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
+ return hole
+
+ return self.cutEach(_makeHole,True)
+
+ #TODO: duplicated code with _extrude and extrude
+ 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
+
+ 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.
+
+ **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,[])
+
+ self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+ #one would think that fusing faces into a compound and then extruding would work,
+ #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
+ #but then cutting it from the main solid fails with BRep_NotDone.
+ #the work around is to extrude each and then join the resulting solids, which seems to work
+
+ #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
+ r = None
+ for ws in wireSets:
+ thisObj = Solid.extrudeLinearWithRotation(ws[0],ws[1:],self.plane.origin, eDir,angleDegrees)
+ if r is None:
+ r = thisObj
+ else:
+ r = r.fuse(thisObj)
+
+ if combine:
+ return self._combineWithBase(r)
+ else:
+ return self.newObject([r])
+
+ def extrude(self,distance,combine=True):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic solid.
+
+ :param distance: the distance to extrude, normal to the workplane plane
+ :type distance: float, negative means opposite the normal direction
+ :param boolean combine: True to combine the resulting solid with parent solids if found.
+ :return: a CQ object with the resulting solid selected.
+
+ extrude always *adds* material to a part.
+
+ The returned object is always a CQ object, and depends on wither combine is True, and
+ whether a context solid is already defined:
+
+ * if combine is False, the new value is pushed onto the stack.
+ * if combine is true, the value is combined with the context solid if it exists,
+ and the resulting solid becomes the new context solid.
+
+ FutureEnhancement:
+ Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane
+ extrude to surface. this is quite tricky since the surface selected may not be planar
+ """
+ r = self._extrude(distance) #returns a Solid ( or a compound if there were multiple )
+ if combine:
+ return self._combineWithBase(r)
+ else:
+ return self.newObject([r])
+
+ 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.
+
+ :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:
+
+ * 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
+
+ #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
+ if axisStart is None:
+ 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
+ if axisEnd is None:
+ #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()
+ else:
+ 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 )
+ if combine:
+ return self._combineWithBase(r)
+ else:
+ return self.newObject([r])
+
+ def _combineWithBase2(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
+
+ """
+ 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 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!
+
+ :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)
+ for ss in items:
+ s = s.fuse(ss)
+
+ return self.newObject([s])
+
+ 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
+
+
+ :param toUnion:
+ :type toUnion: a solid object, or a CQ object having a solid,
+ :raises: ValueError if there is no solid to add to in the chain
+ :return: a CQ object with the resulting object selected
+ """
+
+ #first collect all of the items together
+ if type(toUnion) == CQ or type(toUnion) == Workplane:
+ solids = toUnion.solids().vals()
+ 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:
+ newS = newS.fuse(s)
+ elif type(toUnion) == Solid:
+ newS = toUnion
+ else:
+ 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
+ if combine and solidRef is not None:
+ t = solidRef.fuse(newS)
+ solidRef.wrapped = newS.wrapped
+ return self.newObject([t])
+ else:
+ return self.newObject([newS])
+
+ def cut(self,toCut,combine=True):
+ """
+ Cuts the provided solid from the current solid, IE, perform a solid subtraction
+
+ if combine=True, the result and the original are updated to point to the new object
+ if combine=False, the result will be on the stack, but the original is unmodified
+
+ :param toCut: object to cut
+ :type toCut: a solid object, or a CQ object having a solid,
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ """
+
+ solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from
+
+ if solidRef is None:
+ raise ValueError("Cannot find solid to cut from!!!")
+ solidToCut = None
+ if type(toCut) == CQ or type(toCut) == Workplane:
+ solidToCut = toCut.val()
+ elif type(toCut) == Solid:
+ solidToCut = toCut
+ else:
+ raise ValueError("Cannot cut Type '%s' " % str(type(toCut)))
+
+ newS = solidRef.cut(solidToCut)
+ if combine:
+ solidRef.wrapped = newS.wrapped
+ return self.newObject([newS])
+
+
+ def cutBlind(self,distanceToCut):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material from.
+ cutBlind always removes material from a part.
+
+ :param distanceToCut: distance to extrude before cutting
+ :type distanceToCut: float, >0 means in the positive direction of the workplane normal, <0 means in the negative direction
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutThruAll` to cut material from the entire part
+
+ Future Enhancements:
+ Cut Up to Surface
+ """
+ #first, make the object
+ toCut = self._extrude(distanceToCut)
+
+ #now find a solid in the chain
+
+ solidRef = self.findSolid()
+
+ s= solidRef.cut(toCut)
+ solidRef.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def cutThruAll(self,positive=False):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material from.
+ cutThruAll always removes material from a part.
+
+ :param boolean positive: True to cut in the positive direction, false to cut in the negative direction
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutBlind` to cut material to a limited depth
+
+ """
+ maxDim = self.largestDimension()
+ if not positive:
+ maxDim *= (-1.0)
+
+ return self.cutBlind(maxDim)
+
+
+ def loft(self,filled=True,combine=True):
+ """
+ Make a lofted solid, through the set of wires.
+ :return:
+ """
+ wiresToLoft = self.ctx.pendingWires
+ self.ctx.pendingWires = []
+
+ r = Solid.makeLoft(wiresToLoft)
+
+ if combine:
+ parentSolid = self.findSolid(searchStack=False,searchParents=True)
+ if parentSolid is not None:
+ r = parentSolid.fuse(r)
+ parentSolid.wrapped = r.wrapped
+
+ return self.newObject([r])
+
+ def _extrude(self,distance):
+ """
+ Make a prismatic solid from the existing set of pending wires.
+
+ :param distance: distance to extrude
+ :return: a FreeCAD solid, suitable for boolean operations.
+
+ This method is a utility method, primarily for plugin and internal use.
+ It is the basis for cutBlind,extrude,cutThruAll, and all similar methods.
+
+ Future Enhancements:
+ extrude along a profile ( sweep )
+ """
+
+ #group wires together into faces based on which ones are inside the others
+ #result is a list of lists
+ s = time.time()
+ 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
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+
+ #one would think that fusing faces into a compound and then extruding would work,
+ #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
+ #but then cutting it from the main solid fails with BRep_NotDone.
+ #the work around is to extrude each and then join the resulting solids, which seems to work
+
+ #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
+
+ # 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:
+ thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
+ toFuse.append(thisObj)
+
+ return Compound.makeCompound(toFuse)
+
+ def _revolve(self, angleDegrees, axisStart, axisEnd):
+ """
+ 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.
+
+ 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,[])
+
+ #Mark that all of the wires have been used to create a revolution
+ self.ctx.pendingWires = []
+
+ #Revolve the wires, make a compound out of them and then fuse them
+ toFuse = []
+ for ws in wireSets:
+ thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd)
+ toFuse.append(thisObj)
+
+ return Compound.makeCompound(toFuse)
+
+ 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.
+
+ :param length: box size in X direction
+ :type length: float > 0
+ :param width: box size in Y direction
+ :type width: float > 0
+ :param height: box size in Z direction
+ :type height: float > 0
+ :param centered: should the box be centered, or should reference point be at the lower bound of the range?
+ :param combine: should the results be combined with other solids on the stack ( and each other)?
+ :type combine: true to combine shapes, false otherwise.
+
+ Centered is a tuple that describes whether the box should be centered on the x,y, and z axes. If true,
+ the box is centered on the respective axis relative to the workplane origin, if false, the workplane center
+ will represent the lower bound of the resulting box
+
+ one box is created for each item on the current stack. If no items are on the stack, one box using
+ the current workplane center is created.
+
+ If combine is true, the result will be a single object on the stack:
+ if a solid was found in the chain, the result is that solid with all boxes produced fused onto it
+ otherwise, the result is the combination of all the produced boxes
+
+ if combine is false, the result will be a list of the boxes produced
+
+ Most often boxes form the basis for a part::
+
+ #make a single box with lower left corner at origin
+ s = Workplane().box(1,2,3,centered=(False,False,False)
+
+ But sometimes it is useful to create an array of them:
+
+ #create 4 small square bumps on a larger base plate:
+ s = Workplane().box(4,4,0.5).faces(">Z").workplane()\
+ .rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True)
+
+ """
+
+ def _makebox(pnt):
+
+ #(xp,yp,zp) = self.plane.toLocalCoords(pnt)
+ (xp,yp,zp) = pnt.toTuple()
+ if centered[0]:
+ xp = xp-(length/2.0)
+ if centered[1]:
+ yp = yp-(width/2.0)
+ if centered[2]:
+ zp = zp-(height/2.0)
+
+ return Solid.makeBox(length,width,height,Vector(xp,yp,zp))
+
+ boxes = self.eachpoint(_makebox,True)
+
+ #if combination is not desired, just return the created boxes
+ if not combine:
+ return boxes
+ else:
+ #combine everything
+ return self.union(boxes)
+
diff --git a/CadQuery/Libs/cadquery/README.txt b/CadQuery/Libs/cadquery/README.txt
new file mode 100644
index 0000000..ab8dc7e
--- /dev/null
+++ b/CadQuery/Libs/cadquery/README.txt
@@ -0,0 +1,8 @@
+***
+Core CadQuery implementation.
+
+No files should depend on or import FreeCAD , pythonOCC, or other CAD Kernel libraries!!!
+Dependencies should be on the classes provided by implementation packages, which in turn
+can depend on CAD libraries.
+
+***
\ No newline at end of file
diff --git a/CadQuery/Libs/cadquery/__init__.py b/CadQuery/Libs/cadquery/__init__.py
new file mode 100644
index 0000000..def64bc
--- /dev/null
+++ b/CadQuery/Libs/cadquery/__init__.py
@@ -0,0 +1,19 @@
+#these items point to the freecad implementation
+from .freecad_impl.geom import Plane,BoundBox,Vector,Matrix,sortWiresByBuildOrder
+from .freecad_impl.shapes import Shape,Vertex,Edge,Face,Wire,Solid,Shell,Compound
+from .freecad_impl import exporters
+from .freecad_impl import importers
+
+#these items are the common implementation
+
+#the order of these matter
+from .selectors import NearestToPointSelector,ParallelDirSelector,DirectionSelector,PerpendicularDirSelector,TypeSelector,DirectionMinMaxSelector,StringSyntaxSelector,Selector
+from .CQ import CQ,CQContext,Workplane
+
+
+__all__ = [
+ 'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder',
+ 'Shape','Vertex','Edge','Wire','Solid','Shell','Compound','exporters', 'importers', 'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector','TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins'
+]
+
+__version__ = "0.1.7"
\ No newline at end of file
diff --git a/CadQuery/Libs/cadquery/contrib/__init__.py b/CadQuery/Libs/cadquery/contrib/__init__.py
new file mode 100644
index 0000000..6140351
--- /dev/null
+++ b/CadQuery/Libs/cadquery/contrib/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+"""
diff --git a/CadQuery/Libs/cadquery/cq_directive.py b/CadQuery/Libs/cadquery/cq_directive.py
new file mode 100644
index 0000000..d898d27
--- /dev/null
+++ b/CadQuery/Libs/cadquery/cq_directive.py
@@ -0,0 +1,85 @@
+"""
+A special directive for including a cq object.
+
+"""
+
+import sys, os, shutil, imp, warnings, cStringIO, re,traceback
+
+from cadquery import *
+import StringIO
+from docutils.parsers.rst import directives
+
+
+template = """
+
+.. raw:: html
+
+
+ %(outSVG)s
+
+
+
+
+"""
+template_content_indent = ' '
+
+
+def cq_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+
+ #only consider inline snippets
+ plot_code = '\n'.join(content)
+
+ # Since we don't have a filename, use a hash based on the content
+ #the script must define a variable called 'out', which is expected to
+ #be a CQ object
+ outSVG = "Your Script Did not assign the 'result' variable!"
+
+
+ try:
+ _s = StringIO.StringIO()
+ exec(plot_code)
+
+ exporters.exportShape(result,"SVG",_s)
+ outSVG = _s.getvalue()
+ except:
+ traceback.print_exc()
+ outSVG = traceback.format_exc()
+
+ #now out
+ # Now start generating the lines of output
+ lines = []
+
+ #get rid of new lines
+ outSVG = outSVG.replace('\n','')
+
+ txtAlign = "left"
+ if options.has_key("align"):
+ txtAlign = options['align']
+
+ lines.extend((template % locals()).split('\n'))
+
+ lines.extend(['::', ''])
+ lines.extend([' %s' % row.rstrip()
+ for row in plot_code.split('\n')])
+ lines.append('')
+
+ if len(lines):
+ state_machine.insert_input(
+ lines, state_machine.input_lines.source(0))
+
+ return []
+
+def setup(app):
+ setup.app = app
+ setup.config = app.config
+ setup.confdir = app.confdir
+
+ options = {'height': directives.length_or_unitless,
+ 'width': directives.length_or_percentage_or_unitless,
+ 'align': directives.unchanged
+ }
+
+ app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options)
+
+
diff --git a/CadQuery/Libs/cadquery/freecad_impl/README.txt b/CadQuery/Libs/cadquery/freecad_impl/README.txt
new file mode 100644
index 0000000..34ea788
--- /dev/null
+++ b/CadQuery/Libs/cadquery/freecad_impl/README.txt
@@ -0,0 +1,3 @@
+It is ok for files in this directory to import FreeCAD, FreeCAD.Base, and FreeCAD.Part.
+
+Other modules should _not_ depend on FreeCAD
\ No newline at end of file
diff --git a/CadQuery/Libs/cadquery/freecad_impl/__init__.py b/CadQuery/Libs/cadquery/freecad_impl/__init__.py
new file mode 100644
index 0000000..22773b4
--- /dev/null
+++ b/CadQuery/Libs/cadquery/freecad_impl/__init__.py
@@ -0,0 +1,88 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+"""
+import os, sys
+
+
+def _fc_path():
+ """Find FreeCAD"""
+ _PATH = ""
+ if _PATH:
+ return _PATH
+
+ #look for FREECAD_LIB env variable
+ if os.environ.has_key('FREECAD_LIB'):
+ _PATH = os.environ.get('FREECAD_LIB')
+ if os.path.exists( _PATH):
+ return _PATH
+
+ if sys.platform.startswith('linux'):
+ #Make some dangerous assumptions...
+ for _PATH in [
+ os.path.join(os.path.expanduser("~"), "lib/freecad/lib"),
+ "/usr/local/lib/freecad/lib",
+ "/usr/lib/freecad/lib",
+ ]:
+ if os.path.exists(_PATH):
+ return _PATH
+
+ elif sys.platform.startswith('win'):
+ #try all the usual suspects
+ for _PATH in [
+ "c:/Program Files/FreeCAD0.12/bin",
+ "c:/Program Files/FreeCAD0.13/bin",
+ "c:/Program Files/FreeCAD0.14/bin",
+ "c:/Program Files/FreeCAD0.15/bin",
+ "c:/Program Files/FreeCAD0.16/bin",
+ "c:/Program Files/FreeCAD0.17/bin",
+ "c:/Program Files (x86)/FreeCAD0.12/bin",
+ "c:/Program Files (x86)/FreeCAD0.13/bin",
+ "c:/Program Files (x86)/FreeCAD0.14/bin",
+ "c:/Program Files (x86)/FreeCAD0.15/bin",
+ "c:/Program Files (x86)/FreeCAD0.16/bin",
+ "c:/Program Files (x86)/FreeCAD0.17/bin",
+ "c:/apps/FreeCAD0.12/bin",
+ "c:/apps/FreeCAD0.13/bin",
+ "c:/apps/FreeCAD0.14/bin",
+ "c:/apps/FreeCAD0.15/bin",
+ "c:/apps/FreeCAD0.16/bin",
+ "c:/apps/FreeCAD0.17/bin",
+ "c:/Program Files/FreeCAD 0.12/bin",
+ "c:/Program Files/FreeCAD 0.13/bin",
+ "c:/Program Files/FreeCAD 0.14/bin",
+ "c:/Program Files/FreeCAD 0.15/bin",
+ "c:/Program Files/FreeCAD 0.16/bin",
+ "c:/Program Files/FreeCAD 0.17/bin",
+ "c:/Program Files (x86)/FreeCAD 0.12/bin",
+ "c:/Program Files (x86)/FreeCAD 0.13/bin",
+ "c:/Program Files (x86)/FreeCAD 0.14/bin",
+ "c:/Program Files (x86)/FreeCAD 0.15/bin",
+ "c:/Program Files (x86)/FreeCAD 0.16/bin",
+ "c:/Program Files (x86)/FreeCAD 0.17/bin",
+ "c:/apps/FreeCAD 0.12/bin",
+ "c:/apps/FreeCAD 0.13/bin",
+ "c:/apps/FreeCAD 0.14/bin",
+ "c:/apps/FreeCAD 0.15/bin",
+ "c:/apps/FreeCAD 0.16/bin",
+ "c:/apps/FreeCAD 0.17/bin",
+ ]:
+ if os.path.exists(_PATH):
+ return _PATH
+
+#Make sure that the correct FreeCAD path shows up in Python's system path
+sys.path.insert(0, _fc_path())
\ No newline at end of file
diff --git a/CadQuery/Libs/cadquery/freecad_impl/exporters.py b/CadQuery/Libs/cadquery/freecad_impl/exporters.py
new file mode 100644
index 0000000..0633b4d
--- /dev/null
+++ b/CadQuery/Libs/cadquery/freecad_impl/exporters.py
@@ -0,0 +1,419 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+
+ An exporter should provide functionality to accept a shape, and return
+ a string containing the model content.
+"""
+import cadquery
+
+#from .verutil import fc_import
+#FreeCAD = fc_import("FreeCAD")
+import FreeCAD
+import tempfile,os,StringIO
+
+import Drawing
+#Drawing = fc_import("FreeCAD.Drawing")
+#_FCVER = freecad_version()
+#if _FCVER>=(0,13):
+ #import Drawing as FreeCADDrawing #It's in FreeCAD lib path
+#elif _FCVER>=(0,12):
+ #import FreeCAD.Drawing as FreeCADDrawing
+#else:
+ #raise RuntimeError, "Invalid freecad version: %s" % str(".".join(_FCVER))
+
+
+try:
+ import xml.etree.cElementTree as ET
+except ImportError:
+ import xml.etree.ElementTree as ET
+
+class ExportTypes:
+ STL = "STL"
+ STEP = "STEP"
+ AMF = "AMF"
+ SVG = "SVG"
+ TJS = "TJS"
+
+class UNITS:
+ MM = "mm"
+ IN = "in"
+
+
+def toString(shape,exportType,tolerance=0.1):
+ s= StringIO.StringIO()
+ exportShape(shape,exportType,s,tolerance)
+ return s.getvalue()
+
+def exportShape(shape,exportType,fileLike,tolerance=0.1):
+ """
+ :param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
+ object, the first value is exported
+ :param exportFormat: the exportFormat to use
+ :param tolerance: the tolerance, in model units
+ :param fileLike: a file like object to which the content will be written.
+ The object should be already open and ready to write. The caller is responsible
+ for closing the object
+ """
+
+
+ if isinstance(shape,cadquery.CQ):
+ shape = shape.val()
+
+ if exportType == ExportTypes.TJS:
+ #tessellate the model
+ tess = shape.tessellate(tolerance)
+
+ mesher = JsonMesh() #warning: needs to be changed to remove buildTime and exportTime!!!
+ #add vertices
+ for vec in tess[0]:
+ mesher.addVertex(vec.x, vec.y, vec.z)
+
+ #add faces
+ for f in tess[1]:
+ mesher.addTriangleFace(f[0],f[1], f[2])
+ fileLike.write( mesher.toJson())
+ elif exportType == ExportTypes.SVG:
+ fileLike.write(getSVG(shape.wrapped))
+ elif exportType == ExportTypes.AMF:
+ tess = shape.tessellate(tolerance)
+ aw = AmfWriter(tess).writeAmf(fileLike)
+ else:
+
+ #all these types required writing to a file and then
+ #re-reading. this is due to the fact that FreeCAD writes these
+ (h, outFileName) = tempfile.mkstemp()
+ #weird, but we need to close this file. the next step is going to write to
+ #it from c code, so it needs to be closed.
+ os.close(h)
+
+ if exportType == ExportTypes.STEP:
+ shape.exportStep(outFileName)
+ elif exportType == ExportTypes.STL:
+ shape.wrapped.exportStl(outFileName)
+ else:
+ raise ValueError("No idea how i got here")
+
+ res = readAndDeleteFile(outFileName)
+ fileLike.write(res)
+
+def readAndDeleteFile(fileName):
+ """
+ read data from file provided, and delete it when done
+ return the contents as a string
+ """
+ res = ""
+ with open(fileName,'r') as f:
+ res = f.read()
+
+ os.remove(fileName)
+ return res
+
+
+def guessUnitOfMeasure(shape):
+ """
+ Guess the unit of measure of a shape.
+ """
+ bb = shape.BoundBox
+
+ dimList = [ bb.XLength, bb.YLength,bb.ZLength ]
+ #no real part would likely be bigger than 10 inches on any side
+ if max(dimList) > 10:
+ return UNITS.MM
+
+ #no real part would likely be smaller than 0.1 mm on all dimensions
+ if min(dimList) < 0.1:
+ return UNITS.IN
+
+ #no real part would have the sum of its dimensions less than about 5mm
+ if sum(dimList) < 10:
+ return UNITS.IN
+
+ return UNITS.MM
+
+
+class AmfWriter(object):
+ def __init__(self,tessellation):
+
+ self.units = "mm"
+ self.tessellation = tessellation
+
+ def writeAmf(self,outFile):
+ amf = ET.Element('amf',units=self.units)
+ #TODO: if result is a compound, we need to loop through them
+ object = ET.SubElement(amf,'object',id="0")
+ mesh = ET.SubElement(object,'mesh')
+ vertices = ET.SubElement(mesh,'vertices')
+ volume = ET.SubElement(mesh,'volume')
+
+ #add vertices
+ for v in self.tessellation[0]:
+ vtx = ET.SubElement(vertices,'vertex')
+ coord = ET.SubElement(vtx,'coordinates')
+ x = ET.SubElement(coord,'x')
+ x.text = str(v.x)
+ y = ET.SubElement(coord,'y')
+ y.text = str(v.y)
+ z = ET.SubElement(coord,'z')
+ z.text = str(v.z)
+
+ #add triangles
+ for t in self.tessellation[1]:
+ triangle = ET.SubElement(volume,'triangle')
+ v1 = ET.SubElement(triangle,'v1')
+ v1.text = str(t[0])
+ v2 = ET.SubElement(triangle,'v2')
+ v2.text = str(t[1])
+ v3 = ET.SubElement(triangle,'v3')
+ v3.text = str(t[2])
+
+
+ ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1')
+
+"""
+ Objects that represent
+ three.js JSON object notation
+ https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
+"""
+class JsonMesh(object):
+ def __init__(self):
+
+ self.vertices = [];
+ self.faces = [];
+ self.nVertices = 0;
+ self.nFaces = 0;
+
+ def addVertex(self,x,y,z):
+ self.nVertices += 1;
+ self.vertices.extend([x,y,z]);
+
+ #add triangle composed of the three provided vertex indices
+ def addTriangleFace(self, i,j,k):
+ #first position means justa simple triangle
+ self.nFaces += 1;
+ self.faces.extend([0,int(i),int(j),int(k)]);
+
+ """
+ Get a json model from this model.
+ For now we'll forget about colors, vertex normals, and all that stuff
+ """
+ def toJson(self):
+ return JSON_TEMPLATE % {
+ 'vertices' : str(self.vertices),
+ 'faces' : str(self.faces),
+ 'nVertices': self.nVertices,
+ 'nFaces' : self.nFaces
+ };
+
+
+def getPaths(freeCadSVG):
+ """
+ freeCad svg is worthless-- except for paths, which are fairly useful
+ this method accepts svg from fReeCAD and returns a list of strings suitable for inclusion in a path element
+ returns two lists-- one list of visible lines, and one list of hidden lines
+
+ HACK ALERT!!!!!
+ FreeCAD does not give a way to determine which lines are hidden and which are not
+ the only way to tell is that hidden lines are in a with 0.15 stroke and visible are 0.35 stroke.
+ so we actually look for that as a way to parse.
+
+ to make it worse, elementTree xpath attribute selectors do not work in python 2.6, and we
+ cannot use python 2.7 due to freecad. So its necessary to look for the pure strings! ick!
+ """
+
+ hiddenPaths = []
+ visiblePaths = []
+ if len(freeCadSVG) > 0:
+ #yuk, freecad returns svg fragments. stupid stupid
+ fullDoc = "%s" % freeCadSVG
+ e = ET.ElementTree(ET.fromstring(fullDoc))
+ segments = e.findall(".//g")
+ for s in segments:
+ paths = s.findall("path")
+
+ if s.get("stroke-width") == "0.15": #hidden line HACK HACK HACK
+ mylist = hiddenPaths
+ else:
+ mylist = visiblePaths
+
+ for p in paths:
+ mylist.append(p.get("d"))
+ return (hiddenPaths,visiblePaths)
+ else:
+ return ([],[])
+
+def getSVG(shape,opts=None):
+ """
+ Export a shape to SVG
+ """
+
+ d = {'width':800,'height':240,'marginLeft':200,'marginTop':20}
+
+ if opts:
+ d.update(opts)
+
+ #need to guess the scale and the coordinate center
+ uom = guessUnitOfMeasure(shape)
+
+ width=float(d['width'])
+ height=float(d['height'])
+ marginLeft=float(d['marginLeft'])
+ marginTop=float(d['marginTop'])
+
+ #TODO: provide option to give 3 views
+ viewVector = FreeCAD.Base.Vector(-1.75,1.1,5)
+ (visibleG0,visibleG1,hiddenG0,hiddenG1) = Drawing.project(shape,viewVector)
+
+ (hiddenPaths,visiblePaths) = getPaths(Drawing.projectToSVG(shape,viewVector,"ShowHiddenLines")) #this param is totally undocumented!
+
+ #get bounding box -- these are all in 2-d space
+ bb = visibleG0.BoundBox
+ bb.add(visibleG1.BoundBox)
+ bb.add(hiddenG0.BoundBox)
+ bb.add(hiddenG1.BoundBox)
+
+ #width pixels for x, height pixesl for y
+ unitScale = min( width / bb.XLength * 0.75 , height / bb.YLength * 0.75 )
+
+ #compute amount to translate-- move the top left into view
+ (xTranslate,yTranslate) = ( (0 - bb.XMin) + marginLeft/unitScale ,(0- bb.YMax) - marginTop/unitScale)
+
+ #compute paths ( again -- had to strip out freecad crap )
+ hiddenContent = ""
+ for p in hiddenPaths:
+ hiddenContent += PATHTEMPLATE % p
+
+ visibleContent = ""
+ for p in visiblePaths:
+ visibleContent += PATHTEMPLATE % p
+
+ svg = SVG_TEMPLATE % (
+ {
+ "unitScale" : str(unitScale),
+ "strokeWidth" : str(1.0/unitScale),
+ "hiddenContent" : hiddenContent ,
+ "visibleContent" :visibleContent,
+ "xTranslate" : str(xTranslate),
+ "yTranslate" : str(yTranslate),
+ "width" : str(width),
+ "height" : str(height),
+ "textboxY" :str(height - 30),
+ "uom" : str(uom)
+ }
+ )
+ #svg = SVG_TEMPLATE % (
+ # {"content": projectedContent}
+ #)
+ return svg
+
+
+def exportSVG(shape, fileName):
+ """
+ accept a cadquery shape, and export it to the provided file
+ TODO: should use file-like objects, not a fileName, and/or be able to return a string instead
+ export a view of a part to svg
+ """
+
+ svg = getSVG(shape.val().wrapped)
+ f = open(fileName,'w')
+ f.write(svg)
+ f.close()
+
+
+
+JSON_TEMPLATE= """\
+{
+ "metadata" :
+ {
+ "formatVersion" : 3,
+ "generatedBy" : "ParametricParts",
+ "vertices" : %(nVertices)d,
+ "faces" : %(nFaces)d,
+ "normals" : 0,
+ "colors" : 0,
+ "uvs" : 0,
+ "materials" : 1,
+ "morphTargets" : 0
+ },
+
+ "scale" : 1.0,
+
+ "materials": [ {
+ "DbgColor" : 15658734,
+ "DbgIndex" : 0,
+ "DbgName" : "Material",
+ "colorAmbient" : [0.0, 0.0, 0.0],
+ "colorDiffuse" : [0.6400000190734865, 0.10179081114814892, 0.126246120426746],
+ "colorSpecular" : [0.5, 0.5, 0.5],
+ "shading" : "Lambert",
+ "specularCoef" : 50,
+ "transparency" : 1.0,
+ "vertexColors" : false
+ }],
+
+ "vertices": %(vertices)s,
+
+ "morphTargets": [],
+
+ "normals": [],
+
+ "colors": [],
+
+ "uvs": [[]],
+
+ "faces": %(faces)s
+}
+"""
+
+SVG_TEMPLATE = """
+
+"""
+
+PATHTEMPLATE="\t\t\t\n"
+
diff --git a/CadQuery/Libs/cadquery/freecad_impl/geom.py b/CadQuery/Libs/cadquery/freecad_impl/geom.py
new file mode 100644
index 0000000..1818693
--- /dev/null
+++ b/CadQuery/Libs/cadquery/freecad_impl/geom.py
@@ -0,0 +1,589 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+"""
+
+import math,sys
+#import FreeCAD
+#from .verutil import fc_import
+#FreeCAD = fc_import("FreeCAD")
+import FreeCAD
+#Turns out we don't need the Part module here.
+
+def sortWiresByBuildOrder(wireList,plane,result=[]):
+ """
+ Tries to determine how wires should be combined into faces.
+ Assume:
+ The wires make up one or more faces, which could have 'holes'
+ Outer wires are listed ahead of inner wires
+ there are no wires inside wires inside wires ( IE, islands -- we can deal with that later on )
+ none of the wires are construction wires
+ Compute:
+ one or more sets of wires, with the outer wire listed first, and inner ones
+ Returns, list of lists.
+ """
+ result = []
+
+ remainingWires = list(wireList)
+ while remainingWires:
+ outerWire = remainingWires.pop(0)
+ group = [outerWire]
+ otherWires = list(remainingWires)
+ for w in otherWires:
+ if plane.isWireInside(outerWire,w):
+ group.append(w)
+ remainingWires.remove(w)
+ result.append(group)
+
+ return result
+
+class Vector(object):
+ """
+ Create a 3-dimensional vector
+
+ :param *args: a 3-d vector, with x-y-z parts.
+
+ you can either provide:
+ * a FreeCAD vector
+ * a vector ( in which case it is copied )
+ * a 3-tuple
+ * three float values, x, y, and z
+
+ FreeCAD's vector implementation has a dumb
+ implementation for multiply and add-- they modify the existing
+ value and return a copy as well.
+
+ This vector is immutable-- all mutations return a copy!
+
+ """
+ def __init__(self,*args):
+
+ if len(args) == 3:
+ fV = FreeCAD.Base.Vector(args[0],args[1],args[2])
+ elif len(args) == 1:
+ if type(args[0]) is tuple:
+ fV = FreeCAD.Base.Vector(args[0][0],args[0][1],args[0][2])
+ elif type(args[0] is FreeCAD.Base.Vector):
+ fV = args[0]
+ elif type(args[0] is Vector):
+ fV = args[0].wrapped
+ else:
+ fV = args[0]
+ else:
+ raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple")
+
+ self.wrapped = fV
+ self.Length = fV.Length
+ self.x = fV.x
+ self.y = fV.y
+ self.z = fV.z
+
+ def toTuple(self):
+ return (self.x,self.y,self.z)
+
+ #TODO: is it possible to create a dynamic proxy without all this code?
+ def cross(self,v):
+ return Vector( self.wrapped.cross(v.wrapped))
+
+ def dot(self,v):
+ return self.wrapped.dot(v.wrapped)
+
+ def sub(self,v):
+ return self.wrapped.sub(v.wrapped)
+
+ def add(self,v):
+ return Vector( self.wrapped.add(v.wrapped))
+
+ def multiply(self,scale):
+ """
+ Return self multiplied by the provided scalar
+
+ Note: FreeCAD has a bug here, where the
+ base is also modified
+ """
+ tmp = FreeCAD.Base.Vector(self.wrapped)
+ return Vector( tmp.multiply(scale))
+
+ def normalize(self):
+ """
+ Return normalized version this vector.
+
+ Note: FreeCAD has a bug here, where the
+ base is also modified
+ """
+ tmp = FreeCAD.Base.Vector(self.wrapped)
+ tmp.normalize()
+ return Vector( tmp )
+
+ def Center(self):
+ """
+ The center of myself is myself.
+ Provided so that vectors, vertexes, and other shapes all support a common interface,
+ when Center() is requested for all objects on the stack
+ """
+ return self
+
+ def getAngle(self,v):
+ return self.wrapped.getAngle(v.wrapped)
+
+ def distanceToLine(self):
+ raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
+
+ def projectToLine(self):
+ raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
+
+ def distanceToPlane(self):
+ raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
+
+ def projectToPlane(self):
+ raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
+
+ def __hash__(self):
+ return self.wrapped.__hash__()
+
+ def __add__(self,v):
+ return self.add(v)
+
+ def __len__(self):
+ return self.Length
+
+ def __repr__(self):
+ return self.wrapped.__repr__()
+
+ def __str__(self):
+ return self.wrapped.__str__()
+
+ def __len__(self,other):
+ return self.wrapped.__len__(other)
+
+ def __lt__(self,other):
+ return self.wrapped.__lt__(other)
+
+ def __gt__(self,other):
+ return self.wrapped.__gt__(other)
+
+ def __ne__(self,other):
+ return self.wrapped.__ne__(other)
+
+ def __le__(self,other):
+ return self.wrapped.__le__(other)
+
+ def __ge__(self,other):
+ return self.wrapped.__ge__(other)
+
+ def __eq__(self,other):
+ return self.wrapped.__eq__(other)
+
+class Matrix:
+ """
+ A 3d , 4x4 transformation matrix.
+
+ Used to move geometry in space.
+ """
+ def __init__(self,matrix=None):
+ if matrix == None:
+ self.wrapped = FreeCAD.Base.Matrix()
+ else:
+ self.wrapped = matrix
+
+ def rotateX(self,angle):
+ self.wrapped.rotateX(angle)
+
+ def rotateY(self,angle):
+ self.wrapped.rotateY(angle)
+
+
+class Plane:
+ """
+ A 2d coordinate system in space, with the x-y axes on the a plane, and a particular point as the origin.
+
+ A plane allows the use of 2-d coordinates, which are later converted to global, 3d coordinates when
+ the operations are complete.
+
+ Frequently, it is not necessary to create work planes, as they can be created automatically from faces.
+
+ """
+
+ @classmethod
+ def named(cls,stdName,origin=(0,0,0)):
+ """
+ Create a predefined Plane based on the conventional names.
+
+ :param stdName: one of (XY|YZ|XZ|front|back|left|right|top|bottom
+ :type stdName: string
+ :param origin: the desired origin, specified in global coordinates
+ :type origin: 3-tuple of the origin of the new plane, in global coorindates.
+
+ Available named planes are as follows. Direction references refer to the global
+ directions
+
+ =========== ======= ======= ======
+ Name xDir yDir zDir
+ =========== ======= ======= ======
+ XY +x +y +z
+ YZ +y +z +x
+ XZ +x +z -y
+ front +x +y +z
+ back -x +y -z
+ left +z +y -x
+ right -z +y +x
+ top +x -z +y
+ bottom +x +z -y
+ =========== ======= ======= ======
+ """
+
+ namedPlanes = {
+ #origin, xDir, normal
+ 'XY' : Plane(Vector(origin),Vector((1,0,0)),Vector((0,0,1))),
+ 'YZ' : Plane(Vector(origin),Vector((0,1,0)),Vector((1,0,0))),
+ 'XZ' : Plane(Vector(origin),Vector((1,0,0)),Vector((0,-1,0))),
+ 'front': Plane(Vector(origin),Vector((1,0,0)),Vector((0,0,1))),
+ 'back': Plane(Vector(origin),Vector((-1,0,0)),Vector((0,0,-1))),
+ 'left': Plane(Vector(origin),Vector((0,0,1)),Vector((-1,0,0))),
+ 'right': Plane(Vector(origin),Vector((0,0,-1)),Vector((1,0,0))),
+ 'top': Plane(Vector(origin),Vector((1,0,0)),Vector((0,1,0))),
+ 'bottom': Plane(Vector(origin),Vector((1,0,0)),Vector((0,-1,0)))
+ }
+
+ if namedPlanes.has_key(stdName):
+ return namedPlanes[stdName]
+ else:
+ raise ValueError("Supported names are %s " % str(namedPlanes.keys()) )
+
+ @classmethod
+ def XY(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('XY',origin)
+
+ @classmethod
+ def YZ(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('YZ',origin)
+
+ @classmethod
+ def XZ(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('XZ',origin)
+
+ @classmethod
+ def front(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('front',origin)
+
+ @classmethod
+ def back(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('back',origin)
+
+ @classmethod
+ def left(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('left',origin)
+
+ @classmethod
+ def right(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('right',origin)
+
+ @classmethod
+ def top(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('top',origin)
+
+ @classmethod
+ def bottom(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
+ return Plane.named('bottom',origin)
+
+ def __init__(self, origin, xDir, normal ):
+ """
+ Create a Plane with an arbitrary orientation
+
+ TODO: project x and y vectors so they work even if not orthogonal
+ :param origin: the origin
+ :type origin: a three-tuple of the origin, in global coordinates
+ :param xDir: a vector representing the xDirection.
+ :type xDir: a three-tuple representing a vector, or a FreeCAD Vector
+ :param normal: the normal direction for the new plane
+ :type normal: a FreeCAD Vector
+ :raises: ValueError if the specified xDir is not orthogonal to the provided normal.
+ :return: a plane in the global space, with the xDirection of the plane in the specified direction.
+
+ """
+ self.xDir = xDir.normalize()
+ self.yDir = normal.cross(self.xDir).normalize()
+ self.zDir = normal.normalize()
+
+ #stupid freeCAD!!!!! multiply has a bug that changes the original also!
+ self.invZDir = self.zDir.multiply(-1.0)
+
+ self.setOrigin3d(origin)
+
+
+ def setOrigin3d(self,originVector):
+ """
+ Move the origin of the plane, leaving its orientation and xDirection unchanged.
+ :param originVector: the new center of the plane, *global* coordinates
+ :type originVector: a FreeCAD Vector.
+ :return: void
+
+ """
+ self.origin = originVector
+ self._calcTransforms()
+
+ def setOrigin2d(self,x,y):
+ """
+ Set a new origin based of the plane. The plane's orientation and xDrection are unaffected.
+
+ :param float x: offset in the x direction
+ :param float y: offset in the y direction
+ :return: void
+
+ the new coordinates are specified in terms of the current 2-d system. As an example::
+ p = Plane.XY()
+ p.setOrigin2d(2,2)
+ p.setOrigin2d(2,2)
+
+ results in a plane with its origin at (x,y)=(4,4) in global coordinates. The both operations were relative to
+ local coordinates of the plane.
+
+ """
+ self.setOrigin3d(self.toWorldCoords((x,y)))
+
+
+ def isWireInside(self,baseWire,testWire):
+ """
+ Determine if testWire is inside baseWire, after both wires are projected into the current plane
+
+ :param baseWire: a reference wire
+ :type baseWire: a FreeCAD wire
+ :param testWire: another wire
+ :type testWire: a FreeCAD wire
+ :return: True if testWire is inside baseWire, otherwise False
+
+ If either wire does not lie in the current plane, it is projected into the plane first.
+
+ *WARNING*: This method is not 100% reliable. It uses bounding box tests, but needs
+ more work to check for cases when curves are complex.
+
+ Future Enhancements:
+ * Discretizing points along each curve to provide a more reliable test
+
+ """
+ #TODO: also use a set of points along the wire to test as well.
+ #TODO: would it be more efficient to create objects in the local coordinate system, and then transform to global
+ #coordinates upon extrusion?
+
+ tBaseWire = baseWire.transformGeometry(self.fG)
+ tTestWire = testWire.transformGeometry(self.fG)
+
+ #these bounding boxes will have z=0, since we transformed them into the space of the plane
+ bb = tBaseWire.BoundingBox()
+ tb = tTestWire.BoundingBox()
+
+ #findOutsideBox actually inspects both ways, here we only want to
+ #know if one is inside the other
+ x = BoundBox.findOutsideBox2D(bb,tb)
+ return x == bb
+
+ def toLocalCoords(self,obj):
+ """
+ Project the provided coordinates onto this plane.
+
+ :param obj: an object or vector to convert
+ :type vector: a vector or shape
+ :return: an object of the same type as the input, but converted to local coordinates
+
+
+ Most of the time, the z-coordinate returned will be zero, because most operations
+ based on a plane are all 2-d. Occasionally, though, 3-d points outside of the current plane are transformed.
+ One such example is :py:meth:`Workplane.box`, where 3-d corners of a box are transformed to orient the box in space
+ correctly.
+
+ """
+ if isinstance(obj,Vector):
+ return Vector(self.fG.multiply(obj.wrapped))
+ elif isinstance(obj,Shape):
+ return obj.transformShape(self.rG)
+ else:
+ raise ValueError("Dont know how to convert type %s to local coordinates" % str(type(obj)))
+
+
+ def toWorldCoords(self, tuplePoint):
+ """
+ Convert a point in local coordinates to global coordinates.
+
+ :param tuplePoint: point in local coordinates to convert
+ :type tuplePoint: a 2 or three tuple of float. the third value is taken to be zero if not supplied
+ :return: a Vector in global coordinates
+
+
+ """
+ if len(tuplePoint) == 2:
+ v = Vector(tuplePoint[0], tuplePoint[1], 0)
+ else:
+ v = Vector(tuplePoint[0],tuplePoint[1],tuplePoint[2])
+ return Vector(self.rG.multiply(v.wrapped))
+
+
+ def rotated(self,rotate=(0,0,0)):
+ """
+ returns a copy of this plane, rotated about the specified axes, as measured from horizontal
+
+ Since the z axis is always normal the plane, rotating around Z will always produce a plane
+ that is parallel to this one
+
+ the origin of the workplane is unaffected by the rotation.
+
+ rotations are done in order x,y,z. if you need a different order, manually chain together multiple .rotate()
+ commands
+
+ :param rotate: Vector [xDegrees,yDegrees,zDegrees]
+ :return: a copy of this plane rotated as requested
+ """
+
+ if rotate.__class__.__name__ != 'Vector':
+ rotate = Vector(rotate)
+ #convert to radians
+ rotate = rotate.multiply(math.pi / 180.0 )
+
+ #compute rotation matrix
+ m = FreeCAD.Base.Matrix()
+ m.rotateX(rotate.x)
+ m.rotateY(rotate.y)
+ m.rotateZ(rotate.z)
+
+ #compute the new plane
+ newXdir = Vector(m.multiply(self.xDir.wrapped))
+ newZdir = Vector(m.multiply(self.zDir.wrapped))
+
+ newP= Plane(self.origin,newXdir,newZdir)
+ return newP
+
+ def rotateShapes(self,listOfShapes,rotationMatrix):
+ """
+ rotate the listOfShapes by the rotationMatrix supplied.
+ @param listOfShapes is a list of shape objects
+ @param rotationMatrix is a geom.Matrix object.
+ returns a list of shape objects rotated according to the rotationMatrix
+ """
+
+ #compute rotation matrix ( global --> local --> rotate --> global )
+ #rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
+ rm = self.computeTransform(rotationMatrix)
+
+
+ #There might be a better way, but to do this rotation takes 3 steps
+ #transform geometry to local coordinates
+ #then rotate about x
+ #then transform back to global coordiante
+
+ resultWires = []
+ for w in listOfShapes:
+ mirrored = w.transformGeometry(rotationMatrix.wrapped)
+ resultWires.append(mirrored)
+
+ return resultWires
+
+
+ def _calcTransforms(self):
+ """
+ Computes transformation martrices to convert betwene local and global coordinates
+ """
+ #r is the forward transformation matrix from world to local coordinates
+ #ok i will be really honest-- i cannot understand exactly why this works
+ #something bout the order of the transaltion and the rotation.
+ # the double-inverting is strange, and i dont understand it.
+ r = FreeCAD.Base.Matrix()
+
+ #forward transform must rotate and adjust for origin
+ (r.A11, r.A12, r.A13 ) = (self.xDir.x, self.xDir.y, self.xDir.z )
+ (r.A21, r.A22, r.A23 ) = (self.yDir.x, self.yDir.y, self.yDir.z )
+ (r.A31, r.A32, r.A33 ) = (self.zDir.x, self.zDir.y, self.zDir.z )
+
+ invR = r.inverse()
+ (invR.A14,invR.A24,invR.A34) = (self.origin.x,self.origin.y,self.origin.z)
+
+ ( self.rG,self.fG ) = ( invR,invR.inverse() )
+
+ def computeTransform(self,tMatrix):
+ """
+ Computes the 2-d projection of the supplied matrix
+ """
+
+ rm = self.fG.multiply(tMatrix.wrapped).multiply(self.rG)
+ return Matrix(rm)
+
+class BoundBox(object):
+ "A BoundingBox for an object or set of objects. Wraps the FreeCAD one"
+ def __init__(self,bb):
+ self.wrapped = bb
+ self.xmin = bb.XMin
+ self.xmax = bb.XMax
+ self.xlen = bb.XLength
+ self.ymin = bb.YMin
+ self.ymax = bb.YMax
+ self.ylen = bb.YLength
+ self.zmin = bb.ZMin
+ self.zmax = bb.ZMax
+ self.zlen = bb.ZLength
+ self.center = Vector(bb.Center)
+ self.DiagonalLength = bb.DiagonalLength
+
+ def add(self,obj):
+ """
+ returns a modified (expanded) bounding box
+
+ obj can be one of several things:
+ 1. a 3-tuple corresponding to x,y, and z amounts to add
+ 2. a vector, containing the x,y,z values to add
+ 3. another bounding box, where a new box will be created that encloses both
+
+ this bounding box is not changed
+ """
+ tmp = FreeCAD.Base.BoundBox(self.wrapped)
+ if type(obj) is tuple:
+ tmp.add(obj[0],obj[1],obj[2])
+ elif type(obj) is Vector:
+ tmp.add(obj.fV)
+ elif type(obj) is BoundBox:
+ tmp.add(obj.wrapped)
+
+ return BoundBox(tmp)
+
+ @classmethod
+ def findOutsideBox2D(cls,b1, b2):
+ """
+ compares bounding boxes. returns none if neither is inside the other. returns
+ the outer one if either is outside the other
+
+ BoundBox.isInside works in 3d, but this is a 2d bounding box, so it doesnt work correctly
+ plus, there was all kinds of rounding error in the built-in implementation i do not understand.
+ Here we assume that the b
+ """
+ bb1 = b1.wrapped
+ bb2 = b2.wrapped
+ if bb1.XMin < bb2.XMin and\
+ bb1.XMax > bb2.XMax and\
+ bb1.YMin < bb2.YMin and\
+ bb1.YMax > bb2.YMax:
+ return b1
+
+ if bb2.XMin < bb1.XMin and\
+ bb2.XMax > bb1.XMax and\
+ bb2.YMin < bb1.YMin and\
+ bb2.YMax > bb1.YMax:
+ return b2
+
+ return None
+
+ def isInside(self,anotherBox):
+ """
+ is the provided bounding box inside this one?
+ """
+ return self.wrapped.isInside(anotherBox.wrapped)
diff --git a/CadQuery/Libs/cadquery/freecad_impl/importers.py b/CadQuery/Libs/cadquery/freecad_impl/importers.py
new file mode 100644
index 0000000..c959fb9
--- /dev/null
+++ b/CadQuery/Libs/cadquery/freecad_impl/importers.py
@@ -0,0 +1,64 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+
+ An exporter should provide functionality to accept a shape, and return
+ a string containing the model content.
+"""
+import cadquery
+from .shapes import Shape
+#from .verutil import fc_import
+# FreeCAD = fc_import("FreeCAD")
+# Part = fc_import("FreeCAD.Part")
+import FreeCAD
+import Part
+
+class ImportTypes:
+ STEP = "STEP"
+
+class UNITS:
+ MM = "mm"
+ IN = "in"
+
+def importShape(importType,fileName):
+ """
+ Imports a file based on the type (STEP, STL, etc)
+ :param importType: The type of file that we're importing
+ :param fileName: THe name of the file that we're importing
+ """
+
+ #Check to see what type of file we're working with
+ if importType == ImportTypes.STEP:
+ return importStep(fileName)
+
+#Loads a STEP file into a CQ object
+def importStep(fileName):
+ """
+ Accepts a file name and loads the STEP file into a cadquery shape
+ :param fileName: The path and name of the STEP file to be imported
+ """
+
+ #Now read and return the shape
+ try:
+ rshape = Part.read(fileName)
+
+ r = Shape.cast(rshape)
+ #print "loadStep: " + str(r)
+ #print "Faces=%d" % cadquery.CQ(r).solids().size()
+ return cadquery.CQ(r)
+ except:
+ raise ValueError("STEP File Could not be loaded")
diff --git a/CadQuery/Libs/cadquery/freecad_impl/shapes.py b/CadQuery/Libs/cadquery/freecad_impl/shapes.py
new file mode 100644
index 0000000..b6c1f8a
--- /dev/null
+++ b/CadQuery/Libs/cadquery/freecad_impl/shapes.py
@@ -0,0 +1,859 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+
+ Wrapper Classes for FreeCAD
+ These classes provide a stable interface for 3d objects,
+ independent of the FreeCAD interface.
+
+ Future work might include use of pythonOCC, OCC, or even
+ another CAD kernel directly, so this interface layer is quite important.
+
+ Funny, in java this is one of those few areas where i'd actually spend the time
+ to make an interface and an implementation, but for new these are just rolled together
+
+ This interface layer provides three distinct values:
+
+ 1. It allows us to avoid changing key api points if we change underlying implementations.
+ It would be a disaster if script and plugin authors had to change models because we
+ changed implmentations
+
+ 2. Allow better documentation. One of the reasons FreeCAD is no more popular is because
+ its docs are terrible. This allows us to provie good documentation via docstrings
+ for each wrapper
+
+ 3. Work around bugs. there are a quite a feb bugs in free this layer allows fixing them
+
+ 4. allows for enhanced functionality. Many objects are missing features we need. For example
+ we need a 'forConstruciton' flag on the Wire object. this allows adding those kinds of things
+
+ 5. allow changing interfaces when we'd like. there are few cases where the freecad api is not
+ very userfriendly: we like to change those when necesary. As an example, in the freecad api,
+ all factory methods are on the 'Part' object, but it is very useful to know what kind of
+ object each one returns, so these are better grouped by the type of object they return.
+ (who would know that Part.makeCircle() returns an Edge, but Part.makePolygon() returns a Wire ?
+"""
+from cadquery import Vector, BoundBox
+import FreeCAD
+
+#from .verutil import fc_import
+
+#FreeCADPart = fc_import("FreeCAD.Part")
+import Part as FreeCADPart
+
+
+class Shape(object):
+ """
+ Represents a shape in the system.
+ Wrappers the FreeCAD api
+ """
+
+ def __init__(self, obj):
+ self.wrapped = obj
+ self.forConstruction = False
+
+ @classmethod
+ def cast(cls, obj, forConstruction=False):
+ "Returns the right type of wrapper, given a FreeCAD object"
+ s = obj.ShapeType
+ if type(obj) == FreeCAD.Base.Vector:
+ return Vector(obj)
+ tr = None
+
+ # TODO: there is a clever way to do this i'm sure with a lookup
+ # but it is not a perfect mapping, because we are trying to hide
+ # a bit of the complexity of Compounds in FreeCAD.
+ if s == 'Vertex':
+ tr = Vertex(obj)
+ elif s == 'Edge':
+ tr = Edge(obj)
+ elif s == 'Wire':
+ tr = Wire(obj)
+ elif s == 'Face':
+ tr = Face(obj)
+ elif s == 'Shell':
+ tr = Shell(obj)
+ elif s == 'Solid':
+ tr = Solid(obj)
+ elif s == 'Compound':
+ #compound of solids, lets return a solid instead
+ if len(obj.Solids) > 1:
+ tr = Solid(obj)
+ elif len(obj.Solids) == 1:
+ tr = Solid(obj.Solids[0])
+ elif len(obj.Wires) > 0:
+ tr = Wire(obj)
+ else:
+ tr = Compound(obj)
+ else:
+ raise ValueError("cast:unknown shape type %s" % s)
+
+ tr.forConstruction = forConstruction
+ return tr
+
+ # TODO: all these should move into the exporters folder.
+ # we dont need a bunch of exporting code stored in here!
+ #
+ def exportStl(self, fileName):
+ self.wrapped.exportStl(fileName)
+
+ def exportStep(self, fileName):
+ self.wrapped.exportStep(fileName)
+
+ def exportShape(self, fileName, fileFormat):
+ if fileFormat == ExportFormats.STL:
+ self.wrapped.exportStl(fileName)
+ elif fileFormat == ExportFormats.BREP:
+ self.wrapped.exportBrep(fileName)
+ elif fileFormat == ExportFormats.STEP:
+ self.wrapped.exportStep(fileName)
+ elif fileFormat == ExportFormats.AMF:
+ # not built into FreeCAD
+ #TODO: user selected tolerance
+ tess = self.wrapped.tessellate(0.1)
+ aw = amfUtils.AMFWriter(tess)
+ aw.writeAmf(fileName)
+ elif fileFormat == ExportFormats.IGES:
+ self.wrapped.exportIges(fileName)
+ else:
+ raise ValueError("Unknown export format: %s" % format)
+
+ def geomType(self):
+ """
+ Gets the underlying geometry type
+ :return: a string according to the geometry type.
+
+ Implementations can return any values desired, but the
+ values the user uses in type filters should correspond to these.
+
+ As an example, if a user does::
+
+ CQ(object).faces("%mytype")
+
+ The expectation is that the geomType attribute will return 'mytype'
+
+ The return values depend on the type of the shape:
+
+ Vertex: always 'Vertex'
+ Edge: LINE, ARC, CIRCLE, SPLINE
+ Face: PLANE, SPHERE, CONE
+ Solid: 'Solid'
+ Shell: 'Shell'
+ Compound: 'Compound'
+ Wire: 'Wire'
+ """
+ return self.wrapped.ShapeType
+
+ def isType(self, obj, strType):
+ """
+ Returns True if the shape is the specified type, false otherwise
+
+ contrast with ShapeType, which will raise an exception
+ if the provide object is not a shape at all
+ """
+ if hasattr(obj, 'ShapeType'):
+ return obj.ShapeType == strType
+ else:
+ return False
+
+ def hashCode(self):
+ return self.wrapped.hashCode()
+
+ def isNull(self):
+ return self.wrapped.isNull()
+
+ def isSame(self, other):
+ return self.wrapped.isSame(other.wrapped)
+
+ def isEqual(self, other):
+ return self.wrapped.isEqual(other.wrapped)
+
+ def isValid(self):
+ return self.wrapped.isValid()
+
+ def BoundingBox(self):
+ return BoundBox(self.wrapped.BoundBox)
+
+ def Center(self):
+ try:
+ return Vector(self.wrapped.CenterOfMass)
+ except:
+ pass
+
+ def Closed(self):
+ return self.wrapped.Closed
+
+ def ShapeType(self):
+ return self.wrapped.ShapeType
+
+ def Vertices(self):
+ return [Vertex(i) for i in self.wrapped.Vertexes]
+
+ def Edges(self):
+ return [Edge(i) for i in self.wrapped.Edges]
+
+ def Compounds(self):
+ return [Compound(i) for i in self.wrapped.Compounds]
+
+ def Wires(self):
+ return [Wire(i) for i in self.wrapped.Wires]
+
+ def Faces(self):
+ return [Face(i) for i in self.wrapped.Faces]
+
+ def Shells(self):
+ return [Shell(i) for i in self.wrapped.Shells]
+
+ def Solids(self):
+ return [Solid(i) for i in self.wrapped.Solids]
+
+ def Area(self):
+ return self.wrapped.Area
+
+ def Length(self):
+ return self.wrapped.Length
+
+ def rotate(self, startVector, endVector, angleDegrees):
+ """
+ Rotates a shape around an axis
+ :param startVector: start point of rotation axis either a 3-tuple or a Vector
+ :param endVector: end point of rotation axis, either a 3-tuple or a Vector
+ :param angleDegrees: angle to rotate, in degrees
+ :return: a copy of the shape, rotated
+ """
+ if type(startVector) == tuple:
+ startVector = Vector(startVector)
+
+ if type(endVector) == tuple:
+ endVector = Vector(endVector)
+
+ tmp = self.wrapped.copy()
+ tmp.rotate(startVector.wrapped, endVector.wrapped, angleDegrees)
+ return Shape.cast(tmp)
+
+ def translate(self, vector):
+
+ if type(vector) == tuple:
+ vector = Vector(vector)
+ tmp = self.wrapped.copy()
+ tmp.translate(vector.wrapped)
+ return Shape.cast(tmp)
+
+ def scale(self, factor):
+ tmp = self.wrapped.copy()
+ tmp.scale(factor)
+ return Shape.cast(tmp)
+
+ def copy(self):
+ return Shape.cast(self.wrapped.copy())
+
+ def transformShape(self, tMatrix):
+ """
+ tMatrix is a matrix object.
+ returns a copy of the ojbect, transformed by the provided matrix,
+ with all objects keeping their type
+ """
+ tmp = self.wrapped.copy()
+ tmp.transformShape(tMatrix)
+ r = Shape.cast(tmp)
+ r.forConstruction = self.forConstruction
+ return r
+
+ def transformGeometry(self, tMatrix):
+ """
+ tMatrix is a matrix object.
+
+ returns a copy of the object, but with geometry transformed insetad of just
+ rotated.
+
+ WARNING: transformGeometry will sometimes convert lines and circles to splines,
+ but it also has the ability to handle skew and stretching transformations.
+
+ If your transformation is only translation and rotation, it is safer to use transformShape,
+ which doesnt change the underlying type of the geometry, but cannot handle skew transformations
+ """
+ tmp = self.wrapped.copy()
+ tmp = tmp.transformGeometry(tMatrix)
+ return Shape.cast(tmp)
+
+ def __hash__(self):
+ return self.wrapped.hashCode()
+
+
+class Vertex(Shape):
+ def __init__(self, obj, forConstruction=False):
+ """
+ Create a vertex from a FreeCAD Vertex
+ """
+ self.wrapped = obj
+ self.forConstruction = forConstruction
+ self.X = obj.X
+ self.Y = obj.Y
+ self.Z = obj.Z
+
+ def toTuple(self):
+ return (self.X, self.Y, self.Z)
+
+ def Center(self):
+ """
+ The center of a vertex is itself!
+ """
+ return Vector(self.wrapped.Point)
+
+
+class Edge(Shape):
+ def __init__(self, obj):
+ """
+ An Edge
+ """
+ self.wrapped = obj
+ # self.startPoint = None
+ # self.endPoint = None
+
+ self.edgetypes = {
+ FreeCADPart.Line: 'LINE',
+ FreeCADPart.ArcOfCircle: 'ARC',
+ FreeCADPart.Circle: 'CIRCLE'
+ }
+
+ def geomType(self):
+ t = type(self.wrapped.Curve)
+ if self.edgetypes.has_key(t):
+ return self.edgetypes[t]
+ else:
+ return "Unknown Edge Curve Type: %s" % str(t)
+
+ def startPoint(self):
+ """
+
+ :return: a vector representing the start poing of this edge
+
+ Note, circles may have the start and end points the same
+ """
+ # work around freecad bug where valueAt is unreliable
+ curve = self.wrapped.Curve
+ return Vector(curve.value(self.wrapped.ParameterRange[0]))
+
+ def endPoint(self):
+ """
+
+ :return: a vector representing the end point of this edge.
+
+ Note, circles may have the start and end points the same
+
+ """
+ # warning: easier syntax in freecad of .valueAt(.ParameterRange[1]) has
+ # a bug with curves other than arcs, but using the underlying curve directly seems to work
+ # that's the solution i'm using below
+ curve = self.wrapped.Curve
+ v = Vector(curve.value(self.wrapped.ParameterRange[1]))
+ return v
+
+ def tangentAt(self, locationVector=None):
+ """
+ Compute tangent vector at the specified location.
+ :param locationVector: location to use. Use the center point if None
+ :return: tangent vector
+ """
+ if locationVector is None:
+ locationVector = self.Center()
+
+ p = self.wrapped.Curve.parameter(locationVector.wrapped)
+ return Vector(self.wrapped.tangentAt(p))
+
+ @classmethod
+ def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360):
+ return Edge(FreeCADPart.makeCircle(radius, toVector(pnt), toVector(dir), angle1, angle2))
+
+ @classmethod
+ def makeSpline(cls, listOfVector):
+ """
+ Interpolate a spline through the provided points.
+ :param cls:
+ :param listOfVector: a list of Vectors that represent the points
+ :return: an Edge
+ """
+ vecs = [v.wrapped for v in listOfVector]
+
+ spline = FreeCADPart.BSplineCurve()
+ spline.interpolate(vecs, False)
+ return Edge(spline.toShape())
+
+ @classmethod
+ def makeThreePointArc(cls, v1, v2, v3):
+ """
+ Makes a three point arc through the provided points
+ :param cls:
+ :param v1: start vector
+ :param v2: middle vector
+ :param v3: end vector
+ :return: an edge object through the three points
+ """
+ arc = FreeCADPart.Arc(v1.wrapped, v2.wrapped, v3.wrapped)
+ e = Edge(arc.toShape())
+ return e # arcane and undocumented, this creates an Edge object
+
+ @classmethod
+ def makeLine(cls, v1, v2):
+ """
+ Create a line between two points
+ :param v1: Vector that represents the first point
+ :param v2: Vector that represents the second point
+ :return: A linear edge between the two provided points
+ """
+ return Edge(FreeCADPart.makeLine(v1.toTuple(), v2.toTuple()))
+
+
+class Wire(Shape):
+ def __init__(self, obj):
+ """
+ A Wire
+ """
+ self.wrapped = obj
+
+ @classmethod
+ def combine(cls, listOfWires):
+ """
+ Attempt to combine a list of wires into a new wire.
+ the wires are returned in a list.
+ :param cls:
+ :param listOfWires:
+ :return:
+ """
+ return Shape.cast(FreeCADPart.Wire([w.wrapped for w in listOfWires]))
+
+ @classmethod
+ def assembleEdges(cls, listOfEdges):
+ """
+ Attempts to build a wire that consists of the edges in the provided list
+ :param cls:
+ :param listOfEdges: a list of Edge objects
+ :return: a wire with the edges assembled
+ """
+ fCEdges = [a.wrapped for a in listOfEdges]
+
+ wa = Wire(FreeCADPart.Wire(fCEdges))
+ return wa
+
+ @classmethod
+ def makeCircle(cls, radius, center, normal):
+ """
+ Makes a Circle centered at the provided point, having normal in the provided direction
+ :param radius: floating point radius of the circle, must be > 0
+ :param center: vector representing the center of the circle
+ :param normal: vector representing the direction of the plane the circle should lie in
+ :return:
+ """
+ w = Wire(FreeCADPart.Wire([FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped)]))
+ return w
+
+ @classmethod
+ def makePolygon(cls, listOfVertices, forConstruction=False):
+ # convert list of tuples into Vectors.
+ w = Wire(FreeCADPart.makePolygon([i.wrapped for i in listOfVertices]))
+ w.forConstruction = forConstruction
+ return w
+
+ @classmethod
+ def makeHelix(cls, pitch, height, radius, angle=360.0):
+ """
+ Make a helix with a given pitch, height and radius
+ By default a cylindrical surface is used to create the helix. If
+ the fourth parameter is set (the apex given in degree) a conical surface is used instead'
+ """
+ return Wire(FreeCADPart.makeHelix(pitch, height, radius, angle))
+
+
+class Face(Shape):
+ def __init__(self, obj):
+ """
+ A Face
+ """
+ self.wrapped = obj
+
+ self.facetypes = {
+ # TODO: bezier,bspline etc
+ FreeCADPart.Plane: 'PLANE',
+ FreeCADPart.Sphere: 'SPHERE',
+ FreeCADPart.Cone: 'CONE'
+ }
+
+ def geomType(self):
+ t = type(self.wrapped.Surface)
+ if self.facetypes.has_key(t):
+ return self.facetypes[t]
+ else:
+ return "Unknown Face Surface Type: %s" % str(t)
+
+ def normalAt(self, locationVector=None):
+ """
+ Computes the normal vector at the desired location on the face.
+
+ :returns: a vector representing the direction
+ :param locationVector: the location to compute the normal at. If none, the center of the face is used.
+ :type locationVector: a vector that lies on the surface.
+ """
+ if locationVector == None:
+ locationVector = self.Center()
+ (u, v) = self.wrapped.Surface.parameter(locationVector.wrapped)
+
+ return Vector(self.wrapped.normalAt(u, v).normalize())
+
+ @classmethod
+ def makePlane(cls, length, width, basePnt=None, dir=None):
+ return Face(FreeCADPart.makePlan(length, width, toVector(basePnt), toVector(dir)))
+
+ @classmethod
+ def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None):
+ """
+ 'makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface
+ Create a ruled surface out of two edges or wires. If wires are used then
+ these must have the same
+ """
+ return Shape.cast(FreeCADPart.makeRuledSurface(edgeOrWire1.obj, edgeOrWire2.obj, dist))
+
+ def cut(self, faceToCut):
+ "Remove a face from another one"
+ return Shape.cast(self.obj.cut(faceToCut.obj))
+
+ def fuse(self, faceToJoin):
+ return Shape.cast(self.obj.fuse(faceToJoin.obj))
+
+ def intersect(self, faceToIntersect):
+ """
+ computes the intersection between the face and the supplied one.
+ The result could be a face or a compound of faces
+ """
+ return Shape.cast(self.obj.common(faceToIntersect.obj))
+
+
+class Shell(Shape):
+ def __init__(self, wrapped):
+ """
+ A Shell
+ """
+ self.wrapped = wrapped
+
+ @classmethod
+ def makeShell(cls, listOfFaces):
+ return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces]))
+
+
+class Solid(Shape):
+ def __init__(self, obj):
+ """
+ A Solid
+ """
+ self.wrapped = obj
+
+ @classmethod
+ def isSolid(cls, obj):
+ """
+ Returns true if the object is a FreeCAD solid, false otherwise
+ """
+ if hasattr(obj, 'ShapeType'):
+ if obj.ShapeType == 'Solid' or \
+ (obj.ShapeType == 'Compound' and len(obj.Solids) > 0):
+ return True
+ return False
+
+ @classmethod
+ def makeBox(cls, length, width, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)):
+ """
+ makeBox(length,width,height,[pnt,dir]) -- Make a box located\nin pnt with the d
+ imensions (length,width,height)\nBy default pnt=Vector(0,0,0) and dir=Vector(0,0,1)'
+ """
+ return Shape.cast(FreeCADPart.makeBox(length, width, height, pnt.wrapped, dir.wrapped))
+
+ @classmethod
+ def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
+ """
+ 'makeCone(radius1,radius2,height,[pnt,dir,angle]) --
+ Make a cone with given radii and height\nBy default pnt=Vector(0,0,0),
+ dir=Vector(0,0,1) and angle=360'
+ """
+ return Shape.cast(FreeCADPart.makeCone(radius1, radius2, height, pnt.wrapped, dir.wrapped, angleDegrees))
+
+ @classmethod
+ def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
+ """
+ makeCylinder(radius,height,[pnt,dir,angle]) --
+ Make a cylinder with a given radius and height
+ By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360'
+ """
+ return Shape.cast(FreeCADPart.makeCylinder(radius, height, pnt.wrapped, dir.wrapped, angleDegrees))
+
+ @classmethod
+ def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None):
+ """
+ makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) --
+ Make a torus with agiven radii and angles
+ By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0
+ ,angle1=360 and angle=360'
+ """
+ return Shape.cast(FreeCADPart.makeTorus(radius1, radius2, pnt, dir, angleDegrees1, angleDegrees2))
+
+ @classmethod
+ def sweep(cls, profileWire, pathWire):
+ """
+ make a solid by sweeping the profileWire along the specified path
+ :param cls:
+ :param profileWire:
+ :param pathWire:
+ :return:
+ """
+ # needs to use freecad wire.makePipe or makePipeShell
+ # needs to allow free-space wires ( those not made from a workplane )
+
+ @classmethod
+ def makeLoft(cls, listOfWire):
+ """
+ makes a loft from a list of wires
+ The wires will be converted into faces when possible-- it is presumed that nobody ever actually
+ wants to make an infinitely thin shell for a real FreeCADPart.
+ """
+ # the True flag requests building a solid instead of a shell.
+
+ return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True))
+
+ @classmethod
+ def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None):
+ """
+ 'makeWedge(xmin, ymin, zmin, z2min, x2min,
+ xmax, ymax, zmax, z2max, x2max,[pnt, dir])
+ Make a wedge located in pnt\nBy default pnt=Vector(0,0,0) and dir=Vec
+ tor(0,0,1)'
+ """
+ return Shape.cast(
+ FreeCADPart.makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt, dir))
+
+ @classmethod
+ def makeSphere(cls, radius, pnt=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None):
+ """
+ 'makeSphere(radius,[pnt, dir, angle1,angle2,angle3]) --
+ Make a sphere with a giv
+ en radius\nBy default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360'
+ """
+ return Solid(FreeCADPart.makeSphere(radius, pnt, angleDegrees1, angleDegrees2, angleDegrees3))
+
+ @classmethod
+ def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees):
+ """
+ Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
+
+ Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
+ construction methods used here are different enough that they should be separate.
+
+ At a high level, the steps followed ar:
+ (1) accept a set of wires
+ (2) create another set of wires like this one, but which are transformed and rotated
+ (3) create a ruledSurface between the sets of wires
+ (40 create a shell and compute the resulting object
+
+ :param outerWire: the outermost wire, a cad.Wire
+ :param innerWires: a list of inner wires, a list of cad.Wire
+ :param vecCenter: the center point about which to rotate. the axis of rotation is defined by
+ vecNormal, located at vecCenter. ( a cad.Vector )
+ :param vecNormal: a vector along which to extrude the wires ( a cad.Vector )
+ :param angleDegrees: the angle to rotate through while extruding
+ :return: a cad.Solid object
+ """
+
+ # from this point down we are dealing with FreeCAD wires not cad.wires
+ startWires = [outerWire.wrapped] + [i.wrapped for i in innerWires]
+ endWires = []
+ p1 = vecCenter.wrapped
+ p2 = vecCenter.add(vecNormal).wrapped
+
+ # make translated and rotated copy of each wire
+ for w in startWires:
+ w2 = w.copy()
+ w2.translate(vecNormal.wrapped)
+ w2.rotate(p1, p2, angleDegrees)
+ endWires.append(w2)
+
+ # make a ruled surface for each set of wires
+ sides = []
+ for w1, w2 in zip(startWires, endWires):
+ rs = FreeCADPart.makeRuledSurface(w1, w2)
+ sides.append(rs)
+
+ #make faces for the top and bottom
+ startFace = FreeCADPart.Face(startWires)
+ endFace = FreeCADPart.Face(endWires)
+
+ #collect all the faces from the sides
+ faceList = [startFace]
+ for s in sides:
+ faceList.extend(s.Faces)
+ faceList.append(endFace)
+
+ shell = FreeCADPart.makeShell(faceList)
+ solid = FreeCADPart.makeSolid(shell)
+ return Shape.cast(solid)
+
+ @classmethod
+ def extrudeLinear(cls, outerWire, innerWires, vecNormal):
+ """
+ Attempt to extrude the list of wires into a prismatic solid in the provided direction
+
+ :param outerWire: the outermost wire
+ :param innerWires: a list of inner wires
+ :param vecNormal: a vector along which to extrude the wires
+ :return: a Solid object
+
+ The wires must not intersect
+
+ Extruding wires is very non-trivial. Nested wires imply very different geometry, and
+ there are many geometries that are invalid. In general, the following conditions must be met:
+
+ * all wires must be closed
+ * there cannot be any intersecting or self-intersecting wires
+ * wires must be listed from outside in
+ * more than one levels of nesting is not supported reliably
+
+ This method will attempt to sort the wires, but there is much work remaining to make this method
+ reliable.
+ """
+
+ # one would think that fusing faces into a compound and then extruding would work,
+ # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
+ # but then cutting it from the main solid fails with BRep_NotDone.
+ #the work around is to extrude each and then join the resulting solids, which seems to work
+
+ #FreeCAD allows this in one operation, but others might not
+ freeCADWires = [outerWire.wrapped]
+ for w in innerWires:
+ freeCADWires.append(w.wrapped)
+
+ f = FreeCADPart.Face(freeCADWires)
+ result = f.extrude(vecNormal.wrapped)
+
+ return Shape.cast(result)
+
+ @classmethod
+ def revolve(cls, outerWire, innerWires, angleDegrees, axisStart, axisEnd):
+ """
+ Attempt to revolve the list of wires into a solid in the provided direction
+
+ :param outerWire: the outermost wire
+ :param innerWires: a list of inner 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 Solid object
+
+ The wires must not intersect
+
+ * all wires must be closed
+ * there cannot be any intersecting or self-intersecting wires
+ * wires must be listed from outside in
+ * more than one levels of nesting is not supported reliably
+ * the wire(s) that you're revolving cannot be centered
+
+ This method will attempt to sort the wires, but there is much work remaining to make this method
+ reliable.
+ """
+ freeCADWires = [outerWire.wrapped]
+
+ for w in innerWires:
+ freeCADWires.append(w.wrapped)
+
+ f = FreeCADPart.Face(freeCADWires)
+
+ rotateCenter = FreeCAD.Base.Vector(axisStart)
+ rotateAxis = FreeCAD.Base.Vector(axisEnd)
+
+ #Convert our axis end vector into to something FreeCAD will understand (an axis specification vector)
+ rotateAxis = rotateCenter.sub(rotateAxis)
+
+ #FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation
+ result = f.revolve(rotateCenter, rotateAxis, angleDegrees)
+
+ return Shape.cast(result)
+
+ def tessellate(self, tolerance):
+ return self.wrapped.tessellate(tolerance)
+
+ def intersect(self, toIntersect):
+ """
+ computes the intersection between this solid and the supplied one
+ The result could be a face or a compound of faces
+ """
+ return Shape.cast(self.wrapped.common(toIntersect.wrapped))
+
+ def cut(self, solidToCut):
+ "Remove a solid from another one"
+ return Shape.cast(self.wrapped.cut(solidToCut.wrapped))
+
+ def fuse(self, solidToJoin):
+ return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped))
+
+ def fillet(self, radius, edgeList):
+ """
+ Fillets the specified edges of this solid.
+ :param radius: float > 0, the radius of the fillet
+ :param edgeList: a list of Edge objects, which must belong to this solid
+ :return: Filleted solid
+ """
+ nativeEdges = [e.wrapped for e in edgeList]
+ return Shape.cast(self.wrapped.makeFillet(radius, nativeEdges))
+
+ def shell(self, faceList, thickness, tolerance=0.0001):
+ """
+ make a shelled solid of given by removing the list of faces
+
+ :param faceList: list of face objects, which must be part of the solid.
+ :param thickness: floating point thickness. positive shells outwards, negative shells inwards
+ :param tolerance: modelling tolerance of the method, default=0.0001
+ :return: a shelled solid
+
+ **WARNING** The underlying FreeCAD implementation can very frequently have problems
+ with shelling complex geometries!
+ """
+ nativeFaces = [f.wrapped for f in faceList]
+ return Shape.cast(self.wrapped.makeThickness(nativeFaces, thickness, tolerance))
+
+
+class Compound(Shape):
+ def __init__(self, obj):
+ """
+ An Edge
+ """
+ self.wrapped = obj
+
+ def Center(self):
+ # TODO: compute the weighted average instead of the first solid
+ return self.Solids()[0].Center()
+
+ @classmethod
+ def makeCompound(cls, listOfShapes):
+ """
+ Create a compound out of a list of shapes
+ """
+ solids = [s.wrapped for s in listOfShapes]
+ c = FreeCADPart.Compound(solids)
+ return Shape.cast(c)
+
+ def fuse(self, toJoin):
+ return Shape.cast(self.wrapped.fuse(toJoin.wrapped))
+
+ def tessellate(self, tolerance):
+ return self.wrapped.tessellate(tolerance)
diff --git a/CadQuery/Libs/cadquery/plugins/__init__.py b/CadQuery/Libs/cadquery/plugins/__init__.py
new file mode 100644
index 0000000..227406f
--- /dev/null
+++ b/CadQuery/Libs/cadquery/plugins/__init__.py
@@ -0,0 +1,18 @@
+"""
+ CadQuery
+ Copyright (C) 2014 Parametric Products Intellectual Holdings, LLC
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+"""
diff --git a/CadQuery/Libs/cadquery/selectors.py b/CadQuery/Libs/cadquery/selectors.py
new file mode 100644
index 0000000..95bd196
--- /dev/null
+++ b/CadQuery/Libs/cadquery/selectors.py
@@ -0,0 +1,363 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+"""
+
+import re
+import math
+from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound
+
+class Selector(object):
+ """
+ Filters a list of objects
+
+ Filters must provide a single method that filters objects.
+ """
+ def filter(self,objectList):
+ """
+ Filter the provided list
+ :param objectList: list to filter
+ :type objectList: list of FreeCAD primatives
+ :return: filtered list
+
+ The default implementation returns the original list unfiltered
+
+ """
+ return objectList
+
+class NearestToPointSelector(Selector):
+ """
+ Selects object nearest the provided point.
+
+ If the object is a vertex or point, the distance
+ is used. For other kinds of shapes, the center of mass
+ is used to to compute which is closest.
+
+ Applicability: All Types of Shapes
+
+ Example::
+
+ CQ(aCube).vertices(NearestToPointSelector((0,1,0))
+
+ returns the vertex of the unit cube closest to the point x=0,y=1,z=0
+
+ """
+ def __init__(self,pnt ):
+ self.pnt = pnt
+ def filter(self,objectList):
+
+ def dist(tShape):
+ return tShape.Center().sub(self.pnt).Length
+ #if tShape.ShapeType == 'Vertex':
+ # return tShape.Point.sub(toVector(self.pnt)).Length
+ #else:
+ # return tShape.CenterOfMass.sub(toVector(self.pnt)).Length
+
+ return [ min(objectList,key=dist) ]
+
+
+class BaseDirSelector(Selector):
+ """
+ A selector that handles selection on the basis of a single
+ direction vector
+ """
+ def __init__(self,vector,tolerance=0.0001 ):
+ self.direction = vector
+ self.TOLERANCE = tolerance
+
+ def test(self,vec):
+ "Test a specified vector. Subclasses override to provide other implementations"
+ return True
+
+ def filter(self,objectList):
+ """
+ There are lots of kinds of filters, but
+ for planes they are always based on the normal of the plane,
+ and for edges on the tangent vector along the edge
+ """
+ r = []
+ for o in objectList:
+ #no really good way to avoid a switch here, edges and faces are simply different!
+
+ if type(o) == Face:
+ # a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir
+ normal = o.normalAt(None)
+
+ if self.test(normal):
+ r.append(o)
+ elif type(o) == Edge and o.geomType() == 'LINE':
+ #an edge is parallel to a direction if it is a line, and the line is parallel to the dir
+ tangent = o.tangentAt(None)
+ if self.test(tangent):
+ r.append(o)
+
+ return r
+
+class ParallelDirSelector(BaseDirSelector):
+ """
+ Selects objects parallel with the provided direction
+
+ Applicability:
+ Linear Edges
+ Planar Faces
+
+ Use the string syntax shortcut \|(X|Y|Z) if you want to select
+ based on a cardinal direction.
+
+ Example::
+
+ CQ(aCube).faces(ParallelDirSelector((0,0,1))
+
+ selects faces with a normals in the z direction, and is equivalent to::
+
+ CQ(aCube).faces("|Z")
+ """
+
+ def test(self,vec):
+ return self.direction.cross(vec).Length < self.TOLERANCE
+
+class DirectionSelector(BaseDirSelector):
+ """
+ Selects objects aligned with the provided direction
+
+ Applicability:
+ Linear Edges
+ Planar Faces
+
+ Use the string syntax shortcut +/-(X|Y|Z) if you want to select
+ based on a cardinal direction.
+
+ Example::
+
+ CQ(aCube).faces(DirectionSelector((0,0,1))
+
+ selects faces with a normals in the z direction, and is equivalent to::
+
+ CQ(aCube).faces("+Z")
+ """
+
+ def test(self,vec):
+ return abs(self.direction.getAngle(vec) < self.TOLERANCE)
+
+class PerpendicularDirSelector(BaseDirSelector):
+ """
+ Selects objects perpendicular with the provided direction
+
+ Applicability:
+ Linear Edges
+ Planar Faces
+
+ Use the string syntax shortcut #(X|Y|Z) if you want to select
+ based on a cardinal direction.
+
+ Example::
+
+ CQ(aCube).faces(PerpendicularDirSelector((0,0,1))
+
+ selects faces with a normals perpendicular to the z direction, and is equivalent to::
+
+ CQ(aCube).faces("#Z")
+ """
+
+ def test(self,vec):
+ angle = self.direction.getAngle(vec)
+ r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE )
+ return not r
+
+
+class TypeSelector(Selector):
+ """
+ Selects objects of the prescribed topological type.
+
+ Applicability:
+ Faces: Plane,Cylinder,Sphere
+ Edges: Line,Circle,Arc
+
+ You can use the shortcut selector %(PLANE|SPHERE|CONE) for faces,
+ and %(LINE|ARC|CIRCLE) for edges.
+
+ For example this::
+
+ CQ(aCube).faces ( TypeSelector("PLANE") )
+
+ will select 6 faces, and is equivalent to::
+
+ CQ(aCube).faces( "%PLANE" )
+
+ """
+ def __init__(self,typeString):
+ self.typeString = typeString.upper()
+
+ def filter(self,objectList):
+ r = []
+ for o in objectList:
+ if o.geomType() == self.typeString:
+ r.append(o)
+ return r
+
+class DirectionMinMaxSelector(Selector):
+ """
+ Selects objects closest or farthest in the specified direction
+ Used for faces, points, and edges
+
+ Applicability:
+ All object types. for a vertex, its point is used. for all other kinds
+ of objects, the center of mass of the object is used.
+
+ You can use the string shortcuts >(X|Y|Z) or <(X|Y|Z) if you want to
+ select based on a cardinal direction.
+
+ For example this::
+
+ CQ(aCube).faces ( DirectionMinMaxSelector((0,0,1),True )
+
+ Means to select the face having the center of mass farthest in the positive z direction,
+ and is the same as:
+
+ CQ(aCube).faces( ">Z" )
+
+ Future Enhancements:
+ provide a nicer way to select in arbitrary directions. IE, a bit more code could
+ allow '>(0,0,1)' to work.
+
+ """
+ def __init__(self,vector,directionMax=True):
+ self.vector = vector
+ self.max = max
+ self.directionMax = directionMax
+ def filter(self,objectList):
+
+ #then sort by distance from origin, along direction specified
+ def distance(tShape):
+ return tShape.Center().dot(self.vector)
+ #if tShape.ShapeType == 'Vertex':
+ # pnt = tShape.Point
+ #else:
+ # pnt = tShape.Center()
+ #return pnt.dot(self.vector)
+
+ if self.directionMax:
+ return [ max(objectList,key=distance) ]
+ else:
+ return [ min(objectList,key=distance) ]
+
+
+class StringSyntaxSelector(Selector):
+ """
+ Filter lists objects using a simple string syntax. All of the filters available in the string syntax
+ are also available ( usually with more functionality ) through the creation of full-fledged
+ selector objects. see :py:class:`Selector` and its subclasses
+
+ Filtering works differently depending on the type of object list being filtered.
+
+ :param selectorString: A two-part selector string, [selector][axis]
+
+ :return: objects that match the specified selector
+
+ ***Modfiers*** are ``('|','+','-','<','>','%')``
+
+ :\|:
+ parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects.
+ :#:
+ perpendicular to (same as :py:class:`PerpendicularDirSelector` )
+ :+:
+ positive direction (same as :py:class:`DirectionSelector` )
+ :-:
+ negative direction (same as :py:class:`DirectionSelector` )
+ :>:
+ maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True)
+ :<:
+ minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False )
+ :%:
+ curve/surface type (same as :py:class:`TypeSelector`)
+
+ ***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ``
+
+ Selectors are a complex topic: see :ref:`selector_reference` for more information
+
+
+
+ """
+ def __init__(self,selectorString):
+
+ self.axes = {
+ 'X': Vector(1,0,0),
+ 'Y': Vector(0,1,0),
+ 'Z': Vector(0,0,1),
+ 'XY': Vector(1,1,0),
+ 'YZ': Vector(0,1,1),
+ 'XZ': Vector(1,0,1)
+ }
+
+ namedViews = {
+ 'front': ('>','Z' ),
+ 'back': ('<','Z'),
+ 'left':('<', 'X'),
+ 'right': ('>', 'X'),
+ 'top': ('>','Y'),
+ 'bottom': ('<','Y')
+ }
+ self.selectorString = selectorString
+ r = re.compile("\s*([-\+<>\|\%#])*\s*(\w+)\s*",re.IGNORECASE)
+ m = r.match(selectorString)
+
+ if m != None:
+ if namedViews.has_key(selectorString):
+ (a,b) = namedViews[selectorString]
+ self.mySelector = self._chooseSelector(a,b )
+ else:
+ self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1])
+ else:
+ raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ")
+
+
+ def _chooseSelector(self,selType,selAxis):
+ """Sets up the underlying filters accordingly"""
+
+ if selType == "%":
+ return TypeSelector(selAxis)
+
+ #all other types need to select axis as a vector
+ #get the axis vector first, will throw an except if an unknown axis is used
+ try:
+ vec = self.axes[selAxis]
+ except KeyError:
+ raise ValueError ("Axis value %s not allowed: must be one of %s" % (selAxis, str(self.axes)))
+
+ if selType in (None, "+"):
+ #use direction filter
+ return DirectionSelector(vec)
+ elif selType == '-':
+ #just use the reverse of the direction vector
+ return DirectionSelector(vec.multiply(-1.0))
+ elif selType == "|":
+ return ParallelDirSelector(vec)
+ elif selType == ">":
+ return DirectionMinMaxSelector(vec,True)
+ elif selType == "<":
+ return DirectionMinMaxSelector(vec,False)
+ elif selType == '#':
+ return PerpendicularDirSelector(vec)
+ else:
+ raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ")
+
+ def filter(self,objectList):
+ """
+ selects minimum, maximum, positive or negative values relative to a direction
+ [+\|-\|<\|>\|] \
+ """
+ return self.mySelector.filter(objectList)
diff --git a/CadQuery/Libs/cadquery/workplane.py b/CadQuery/Libs/cadquery/workplane.py
new file mode 100644
index 0000000..90e621c
--- /dev/null
+++ b/CadQuery/Libs/cadquery/workplane.py
@@ -0,0 +1,1424 @@
+"""
+ Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC
+
+ This file is part of CadQuery.
+
+ CadQuery is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ CadQuery is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
+"""
+
+import math
+
+from cadquery import Vector,CQ,CQContext,Plane,Wire
+
+class Workplane(CQ):
+ """
+ Defines a coordinate system in space, in which 2-d coordinates can be used.
+
+ :param plane: the plane in which the workplane will be done
+ :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
+ :raises: ValueError if the provided plane is not a plane, a valid named workplane
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane, and the
+ *current point* is on the origin.
+
+ .. note::
+ You can also create workplanes on the surface of existing faces using
+ :py:meth:`CQ.workplane`
+
+
+ """
+
+ FOR_CONSTRUCTION = 'ForConstruction'
+
+
+ def __init__(self, inPlane ,origin=(0,0,0), obj=None):
+ """
+ make a workplane from a particular plane
+
+ :param plane: the plane in which the workplane will be done
+ :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
+ :raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane, and the
+ *current point* is on the origin.
+ """
+
+ if inPlane.__class__.__name__ == 'Plane':
+ tmpPlane = inPlane
+ elif type(inPlane) == str:
+ tmpPlane = Plane.named(inPlane,origin)
+ else:
+ tmpPlane = None
+
+ if tmpPlane == None:
+ raise ValueError(" Provided value %s is not a valid work plane." % str(inPlane))
+
+ self.obj = obj
+ self.plane = tmpPlane
+ self.firstPoint = None
+ self.objects = [self.plane.origin] #changed so that workplane has the center as the first item on the stack
+ self.parent = None
+ self.ctx = CQContext()
+
+ def transformed(self,rotate=Vector(0,0,0),offset=Vector(0,0,0)):
+ """
+ Create a new workplane based on the current one.
+ The origin of the new plane is located at the existing origin+offset vector, where offset is given in
+ coordinates local to the current plane
+ The new plane is rotated through the angles specified by the components of the rotation vector
+ :param rotate: vector of angles to rotate, in degrees relative to work plane coordinates
+ :param offset: vector to offset the new plane, in local work plane coordinates
+ :return: a new work plane, transformed as requested
+ """
+ p = self.plane.rotated(rotate)
+ p.setOrigin3d(self.plane.toWorldCoords(offset.toTuple() ))
+ ns = self.newObject([p.origin])
+ ns.plane = p
+
+ return ns
+
+ def newObject(self,objlist):
+ """
+ Create a new workplane object from this one.
+
+ Overrides CQ.newObject, and should be used by extensions, plugins, and
+ subclasses to create new objects.
+
+ :param objlist: new objects to put on the stack
+ :type objlist: a list of CAD primitives
+ :return: a new Workplane object with the current workplane as a parent.
+
+ """
+
+ #copy the current state to the new object
+ ns = Workplane("XY")
+ ns.plane = self.plane
+ ns.parent = self
+ ns.objects = list(objlist)
+ ns.ctx = self.ctx
+ return ns
+
+ def _findFromPoint(self,useLocalCoords=False):
+ """
+ finds the start point for an operation when an existing point
+ is implied. Examples include 2d operations such as lineTo,
+ which allows specifying the end point, and implicitly use the
+ end of the previous line as the starting point
+
+ :return: a Vector representing the point to use, or none if
+ such a point is not available.
+
+ :param useLocalCoords: selects whether the point is returned
+ in local coordinates or global coordinates.
+
+ The algorithm is this:
+ * If an Edge is on the stack, its end point is used.yp
+ * if a vector is on the stack, it is used
+
+ WARNING: only the first object on the stack is used.
+
+ NOTE:
+ """
+ obj = self.objects[0]
+ p = None
+ if isinstance(obj,Edge):
+ p = obj.endPoint()
+ elif isinstance(obj,Vector):
+ p = obj
+ else:
+ raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj))
+
+ if useLocalCoords:
+ return self.plane.toLocalCoords(p)
+ else:
+ return p
+
+ def 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
+
+ :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
+ """
+
+ 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
+ for x in range(xCount):
+ for y in range(yCount):
+ lpoints.append( (xSpacing*(x), ySpacing*(y)) )
+
+ #shift points down and left relative to origin if requested
+ if center:
+ xc = xSpacing*(xCount-1) * 0.5
+ yc = ySpacing*(yCount-1) * 0.5
+ cpoints = []
+ for p in lpoints:
+ cpoints.append( ( p[0] - xc, p[1] - yc ))
+ lpoints = list(cpoints)
+
+ return self.pushPoints(lpoints)
+
+ 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
+
+ :param pntList: a list of points to push onto the stack
+ :type pntList: list of 2-tuples, in *local* coordinates
+ :return: a new workplane with the desired points on the stack.
+
+ A common use is to provide a list of points for a subsequent operation, such as creating circles or holes.
+ This example creates a cube, and then drills three holes through it, based on three points::
+
+ s = Workplane().box(1,1,1).faces(">Z").workplane().pushPoints([(-0.3,0.3),(0.3,0.3),(0,0)])
+ body = s.circle(0.05).cutThruAll()
+
+ Here the circle function operates on all three points, and is then extruded to create three holes.
+ See :py:meth:`circle` for how it works.
+
+ """
+ vecs = []
+ for pnt in pntList:
+ vec = self.plane.toWorldCoords(pnt)
+ vecs.append(vec)
+
+ return self.newObject(vecs)
+
+ def center(self,x,y):
+ """
+ Shift local coordinates to the specified location.
+
+ The location is specified in terms of local coordinates.
+
+ :param float x: the new x location
+ :param float y: the new y location
+ :returns: the workplane object, with the center adjusted.
+
+ The current point is set to the new center.
+ This method is useful to adjust the center point after it has been created automatically on a face,
+ but not where you'd like it to be.
+
+ In this example, we adjust the workplane center to be at the corner of a cube, instead of
+ the center of a face, which is the default::
+
+ #this workplane is centered at x=0.5,y=0.5, the center of the upper face
+ s = Workplane().box(1,1,1).faces(">Z").workplane()
+
+ s.center(-0.5,-0.5) # move the center to the corner
+ t = s.circle(0.25).extrude(0.2)
+ assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner
+
+ The result is a cube with a round boss on the corner
+
+ """
+ "Shift local coordinates to the specified location, according to current coordinates"
+ self.plane.setOrigin2d(x,y)
+ n = self.newObject([self.plane.origin])
+ return n
+
+ def lineTo(self, x, y,forConstruction=False):
+ """
+ Make a line from the current point to the provided point
+
+ :param float x: the x point, in workplane plane coordinates
+ :param float y: the y point, in workplane plane coordinates
+ :return: the Workplane object with the current point at the end of the new line
+
+ see :py:meth:`line` if you want to use relative dimensions to make a line instead.
+
+ """
+ startPoint = self._findFromPoint(False)
+
+ endPoint = self.plane.toWorldCoords((x, y))
+
+ p = Edge.makeLine(startPoint,endPoint)
+
+ if not forConstruction:
+ self._addPendingEdge(p)
+
+ return self.newObject([p])
+
+ #line a specified incremental amount from current point
+ def line(self, xDist, yDist ,forConstruction=False):
+ """
+ Make a line from the current point to the provided point, using dimensions relative to the current point
+
+ :param float xDist: x distance from current point
+ :param float yDist: y distance from current point
+ :return: the workplane object with the current point at the end of the new line
+
+ see :py:meth:`lineTo` if you want to use absolute coordinates to make a line instead.
+
+ """
+ p = self._findFromPoint(True) #return local coordinates
+ return self.lineTo(p.x + xDist, yDist + p.y,forConstruction)
+
+ def vLine(self, distance,forConstruction=False):
+ """
+ Make a vertical line from the current point the provided distance
+
+ :param float distance: (y) distance from current point
+ :return: the workplane object with the current point at the end of the new line
+
+ """
+ return self.line(0, distance,forConstruction)
+
+ def vLineTo(self,yCoord,forConstruction=False):
+ """
+ Make a vertcial line from the current point to the provided y coordinate.
+
+ Useful if it is more convienient to specify the end location rather than distance,
+ as in :py:meth:`vLine`
+
+ :param float yCoord: y coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(p.x,yCoord,forConstruction)
+
+ def hLineTo(self,xCoord,forConstruction=False):
+ """
+ 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
+
+ """
+ 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)
+
+ #absolute move in current plane, not drawing
+ def moveTo(self, x=0, y=0):
+ """
+ Move to the specified point, without drawing.
+
+ :param x: desired x location, in local coordinates
+ :type x: float, or none for zero
+ :param y: desired y location, in local coorindates
+ :type y: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`move` to do the same thing but using relative dimensions
+ """
+ newCenter = Vector(x,y,0)
+ return self.newObject([self.plane.toWorldCoordinates(newCenter)])
+
+ #relative move in current plane, not drawing
+ def move(self, xDist=0, yDist=0):
+ """
+ Move the specified distance from the current point, without drawing.
+
+ :param xDist: desired x distance, in local coordinates
+ :type xDist: float, or none for zero
+ :param yDist: desired y distance, in local coorindates
+ :type yDist: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`moveTo` to do the same thing but using absolute coordinates
+ """
+ p = self._findFromPoint(True)
+ newCenter = p + Vector(xDist,yDist,0)
+ return self.newObject([self.plane.toWorldCoordinates(newCenter)])
+
+
+ def spline(self,listOfXYTuple,forConstruction=False):
+ """
+ Create a spline interpolated through the provided points.
+
+ :param listOfXYTuple: points to interpolate through
+ :type listOfXYTuple: list of 2-tuple
+ :return: a Workplane object with the current point at the end of the spline
+
+ The spline will begin at the current point, and
+ end with the last point in the XY typle list
+
+ This example creates a block with a spline for one side::
+
+ s = Workplane(Plane.XY())
+ sPnts = [
+ (2.75,1.5),
+ (2.5,1.75),
+ (2.0,1.5),
+ (1.5,1.0),
+ (1.0,1.25),
+ (0.5,1.0),
+ (0,1.0)
+ ]
+ r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close()
+ r = r.extrude(0.5)
+
+ *WARNING* It is fairly easy to create a list of points
+ that cannot be correctly interpreted as a spline.
+
+ Future Enhancements:
+ * provide access to control points
+
+ """
+ gstartPoint = self._findFromPoint(False)
+ gEndPoint = self.plane.toWorldCoords(listOfXYTuple[-1])
+
+ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
+ allPoints = [gstartPoint] + vecs
+
+ e = Edge.makeSpline(allPoints)
+
+ if not forConstruction:
+ self._addPendingEdge(e)
+
+ return self.newObject([e])
+
+ def threePointArc(self,point1, point2,forConstruction=False):
+ """
+ Draw an arc from the current point, through point1, and ending at point2
+
+ :param point1: point to draw through
+ :type point1: 2-tuple, in workplane coordinates
+ :param point2: end point for the arc
+ :type point2: 2-tuple, in workplane coordinates
+ :return: a workplane with the current point at the end of the arc
+
+ Future Enhancments:
+ provide a version that allows an arc using relative measures
+ provide a centerpoint arc
+ provide tangent arcs
+
+ """
+
+ gstartPoint = self._findFromPoint(False)
+ gpoint1 = self.plane.toWorldCoords(point1)
+ gpoint2 = self.plane.toWorldCoords(point2)
+
+ arc = Edge.makeThreePointArc(gstartPoint,gpoint1,gpoint2)
+
+ if not forConstruction:
+ self._addPendingEdge(arc)
+
+ return self.newObject([arc])
+
+ def rotateAndCopy(self,matrix):
+ """
+ Makes a copy of all edges on the stack, rotates them according to the
+ provided matrix, and then attempts to consolidate them into a single wire.
+
+ :param matrix: a 4xr transformation matrix, in global coordinates
+ :type matrix: a FreeCAD Base.Matrix object
+ :return: a cadquery object with consolidated wires, and any originals on the stack.
+
+ The most common use case is to create a set of open edges, and then mirror them
+ around either the X or Y axis to complete a closed shape.
+
+ see :py:meth:`mirrorX` and :py:meth:`mirrorY` to mirror about the global X and Y axes
+ see :py:meth:`mirrorX` and for an example
+
+ Future Enhancements:
+ faster implementation: this one transforms 3 times to accomplish the result
+ """
+
+ #compute rotation matrix ( global --> local --> rotate --> global )
+ rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
+
+ #convert edges to a wire, if there are pending edges
+ n = self.wire(forConstruction=False)
+
+ #attempt to consolidate wires together.
+ consolidated = n.consolidateWires()
+
+ #ok, mirror all the wires.
+
+ #There might be a better way, but to do this rotation takes 3 steps
+ #transform geometry to local coordinates
+ #then rotate about x
+ #then transform back to global coordiante
+ originalWires = consolidated.wires().vals()
+ for w in originalWires:
+ mirrored = w.transformGeometry(rm)
+ consolidated.objects.append(mirrored)
+ consolidated._addPendingWire(mirrored)
+
+ #attempt again to consolidate all of the wires
+ c = consolidated.consolidateWires()
+ #c = consolidated
+ return c
+
+ def mirrorY(self):
+ """
+ Mirror entities around the y axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier. This line of code::
+
+ s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25)
+
+ Produces a flat, heart shaped object
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+
+ """
+ tm = Base.Matrix()
+ tm.rotateY(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def mirrorX(self):
+ """
+ Mirror entities around the x axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier.
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+
+ """
+ tm = Base.Matrix()
+ tm.rotateX(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def _addPendingEdge(self,edge):
+ """
+ Queues an edge for later combination into a wire.
+
+ :param edge:
+ :return:
+ """
+ self.ctx.pendingEdges.append(edge)
+
+ if self.ctx.firstPoint is None:
+ self.ctx.firstPoint = edge.startPoint()
+
+ def _addPendingWire(self,wire):
+ """
+ Queue a Wire for later extrusion
+
+ Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids.
+
+ but users do not normally care about these distinctions. Users 'think' in terms
+ of edges, and solids.
+
+ CadQuery tracks edges as they are drawn, and automatically combines them into wires
+ when the user does an operation that needs it.
+
+ Similarly, cadQuery tracks pending wires, and automaticlaly combines them into faces
+ when necessary to make a solid.
+ """
+ self.ctx.pendingWires.append(wire)
+
+
+ def consolidateWires(self):
+ """
+ Attempt to consolidate wires on the stack into a single.
+ If possible, a new object with the results are returned.
+ if not possible, the wires remain separated
+
+ FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly somtimes
+ Additionally, it has a bug where a profile compose of two wires ( rathre than one )
+ also does not work properly
+
+ together these are a real problem.
+ """
+ wires = self.wires().vals()
+ if len(wires) < 2:
+ return self
+
+ #TODO: this makes the assumption that either all wires could be combined, or none.
+ #in reality trying each combination of wires is probably not reasonable anyway
+ w = Wire.combine(wires)
+
+ #ok this is a little tricky. if we consolidate wires, we have to actually
+ #modify the pendingWires collection to remove the original ones, and replace them
+ #with the consolidate done
+ #since we are already assuming that all wires could be consolidated, its easy, we just
+ #clear the pending wire list
+ r = self.newObject([w])
+ r.ctx.pendingWires = []
+ r._addPendingWire(w)
+ return r
+
+
+
+ def wire(self,forConstruction=False):
+ """
+ Returns a CQ object with all pending edges connected into a wire.
+
+ All edges on the stack that can be combined will be combined into a single wire object,
+ and other objects will remain on the stack unmodified
+
+ :param forConstruction: whether the wire should be used to make a solid, or if it is just for reference
+ :type forConstruction: boolean. true if the object is only for reference
+
+ This method is primarily of use to plugin developers making utilites for 2-d construction. This method
+ shoudl be called when a user operation implies that 2-d construction is finished, and we are ready to
+ begin working in 3d
+
+ SEE '2-d construction concepts' for a more detailed explanation of how cadquery handles edges, wires, etc
+
+ Any non edges will still remain.
+ """
+
+ edges = self.ctx.pendingEdges
+
+ #do not consolidate if there are no free edges
+ if len(edges) == 0:
+ return self
+
+ self.ctx.pendingEdges = []
+
+ others = []
+ for e in self.objects:
+ if type(e) != Edge:
+ others.append(e)
+
+
+ w = Wire.assembleEdges(edges)
+ if not forConstruction:
+ self._addPendingWire(w)
+
+ return self.newObject(others + [w])
+
+ def each(self,callBackFunction,useLocalCoordinates=False):
+ """
+ runs the provided function on each value in the stack, and collects the return values into a new CQ
+ object.
+
+ Special note: a newly created workplane always has its center point as its only stack item
+
+ :param callBackFunction: the function to call for each item on the current stack.
+ :param useLocalCoordinates: should values be converted from local coordinates first?
+ :type useLocalCoordinates: boolean
+
+ The callback function must accept one argument, which is the item on the stack, and return
+ one object, which is collected. If the function returns None, nothing is added to the stack.
+ The object passed into the callBackFunction is potentially transformed to local coordinates, if
+ useLocalCoordinates is true
+
+ useLocalCoordinates is very useful for plugin developers.
+
+ If false, the callback function is assumed to be working in global coordinates. Objects created are added
+ as-is, and objects passed into the function are sent in using global coordinates
+
+ If true, the calling function is assumed to be working in local coordinates. Objects are transformed
+ to local coordinates before they are passed into the callback method, and result objects are transformed
+ to global coorindates after they are returned.
+
+ This allows plugin developers to create objects in local coordinates, without worrying
+ about the fact that the working plane is different than the global coordinate system.
+
+
+ TODO: wrapper object for Wire will clean up forConstruction flag everywhere
+
+ """
+ results = []
+ for obj in self.objects:
+
+ if useLocalCoordinates:
+ #TODO: this needs to work for all types of objects, not just vectors!
+ r = callBackFunction(self.plane.toLocalCoords(obj))
+ r = r.transformShape(self.plane.rG)
+ else:
+ r = callBackFunction(obj)
+
+
+ if type(r) == Wire:
+ if not r.forConstruction:
+ self._addPendingWire(r)
+
+ results.append ( r )
+
+
+ return self.newObject(results)
+
+ def eachpoint(self,callbackFunction, useLocalCoordinates=False):
+ """
+ Same as each(), except each item on the stack is converted into a point before it
+ is passed into the callback function.
+
+ :return: CadQuery object which contains a list of vectors (points ) on its stack.
+
+ :param useLocalCoordinates: should points be in local or global coordinates
+ :type useLocalCoordinates: boolean
+
+ The resulting object has a point on the stack for each object on the original stack.
+ Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted
+ to a point by using their center of mass.
+
+ If the stack has zero length, a single point is returned, which is the center of the current
+ workplane/coordinate system
+
+ """
+ #convert stack to a list of points
+ pnts = []
+ if len(self.objects) == 0:
+ #nothing on the stack. here, we'll assume we should operate with the
+ #origin as the context point
+ pnts.append(self.plane.origin)
+ else:
+
+ for v in self.objects:
+ pnts.append(v.Center())
+
+ return self.newObject(pnts).each(callbackFunction,useLocalCoordinates )
+
+
+ #make a rectangle
+ def rect(self,xLen,yLen,centered=True,forConstruction=False):
+ """
+ Make a rectangle for each item on the stack.
+
+ :param xLen: length in xDirection ( in workplane coordinates )
+ :type xLen: float > 0
+ :param yLen: length in yDirection ( in workplane coordinates )
+ :type yLen: float > 0
+ :param boolean centered: true if the rect is centered on the reference point, false if the lower-left is on the reference point
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating part geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+
+ """
+ def makeRectangleWire(pnt):
+ #here pnt is in local coordinates due to useLocalCoords=True
+ (xc,yc,zc) = pnt.toTuple()
+ if centered:
+ p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0,0) )
+ p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0,0) )
+ p3 = pnt.add(Vector(xLen/2.0, yLen/2.0,0) )
+ p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0,0) )
+ else:
+ p1 = pnt
+ p2 = pnt.add(Vector(xLen,0,0))
+ p3 = pnt.add(Vector( xLen,yLen,0 ))
+ p4 = pnt.add(Vector(0,yLen,0))
+ p4 = pnt.add(Vector(0,yLen,0))
+
+ w = Wire.makePolygon([p1,p2,p3,p4,p1],forConstruction)
+ return w
+ #return Part.makePolygon([p1,p2,p3,p4,p1])
+
+ return self.eachpoint(makeRectangleWire,True)
+
+ #circle from current point
+ def circle(self,radius,forConstruction=False):
+ """
+ Make a circle for each item on the stack.
+
+ :param radius: radius of the circle
+ :type radius: float > 0
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating part geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin. Another common case is to use
+ successive circle() calls to create concentric circles. This works because the center of a circle
+ is its reference point::
+
+ s = Workplane().circle(2.0).circle(1.0)
+
+ Creates two concentric circles, which when extruded will form a ring.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+
+ """
+ def makeCircleWire(obj):
+ cir = Wire.makeCircle(radius,obj,Vector(0,0,1))
+ cir.forConstruction = forConstruction
+ return cir
+
+ return self.eachpoint(makeCircleWire,useLocalCoordinates=True)
+
+ def polygon(self,nSides,diameter):
+ """
+ Creates a polygon incsribed in a circle of the specified diamter for each point on the stack
+
+ The first vertex is always oriented in the x direction.
+
+ :param nSides: number of sides, must be > 3
+ :param diameter: the size of the circle the polygon is incribed into
+ :return: a polygon wire
+
+
+ """
+ def _makePolygon(center):
+ #pnt is a vector in local coordinates
+ angle = 2.0 *math.pi / nSides
+ pnts = []
+ for i in range(nSides+1):
+ pnts.append( center + Vector((diameter / 2.0 * math.cos(angle*i)),(diameter / 2.0 * math.sin(angle*i)),0))
+ return Wire.makePolygon(pnts)
+
+ return self.eachpoint(_makePolygon,True)
+
+ def polyline(self,listOfXYTuple,forConstruction=False):
+ """
+ Create a polyline from a list of points
+
+ :param listOfXYTuple: a list of points in Workplane coordinates
+ :type listOfXYTuple: list of 2-tuples
+ :param forConstruction: should the new wire be reference geometry only?
+ :type forConstruction: true if the wire is for reference, false if they are creating part geometry
+ :return: a new CQ object with the new wire on the stack
+
+ *NOTE* most commonly, the resulting wire should be closed.
+
+ Future Enhacement:
+ This should probably yield a list of edges, not a wire, so that
+ it is possible to combine a polyline with other edges and arcs
+ """
+ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
+ w = Wire.makePolygon(vecs)
+ if not forConstruction:
+ self._addPendingWire(w)
+
+ return self.newObject([w])
+
+ #finish a set of lines.
+ #
+ def close(self):
+ """
+ End 2-d construction, and attempt to build a closed wire.
+
+ :return: a CQ object with a completed wire on the stack, if possible.
+
+ After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary
+ to convert the edges produced by these into one or more wires.
+
+ When a set of edges is closed, cadQuery assumes it is safe to build the group of edges
+ into a wire. This example builds a simple triangular prism::
+
+ s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2)
+
+ """
+ self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y)
+ return self.wire()
+
+ def largestDimension(self):
+ """
+ Finds the largest dimension in the stack.
+ Used internally to create thru features, this is how you can compute
+ how long or wide a feature must be to make sure to cut through all of the material
+ :return:
+ """
+ #TODO: this implementation is naive and returns the dims of the first solid... most of
+ #the time this works. but a stronger implementation would be to search all solids.
+ s = self.findSolid()
+ if s:
+ return s.BoundingBox().DiagonalLength * 5.0
+ else:
+ return 1000000
+
+ def cutEach(self,fcn,useLocalCoords=False):
+ """
+ Evaluates the provided function at each point on the stack ( ie, eachpoint )
+ and then cuts the result from the context solid.
+ :param function: a function suitable for use in the eachpoint method: ie, that accepts a vector
+ :param useLocalCoords: same as for :py:meth:`eachpoint`
+ :return: a CQ object that contains the resulting solid
+ :raises: an error if there is not a context solid to cut from
+ """
+ ctxSolid = self.findSolid()
+ if ctxSolid is None:
+ raise ValueError ("Must have a solid in the chain to cut from!")
+
+ #will contain all of the counterbores as a single compound
+ results = self.eachpoint(fcn,useLocalCoords).vals()
+ s = ctxSolid
+ for cb in results:
+ s = s.cut(cb)
+
+ ctxSolid.wrapped = s.wrapped
+ return self.newObject([s])
+
+ #but parameter list is different so a simple function pointer wont work
+ def cboreHole(self,diameter,cboreDiameter,cboreDepth,depth=None):
+ """
+ Makes a counterbored hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param cboreDiameter: the diameter of the cbore
+ :type cboreDiameter: float > 0 and > diameter
+ :param cboreDepth: depth of the counterbore
+ :type cboreDepth: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane plane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().cboreHole(0.125, 0.25,0.125,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. Counterbored holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cskHole` to make countersinks instead of counterbores
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCbore(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0,0,-1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
+
+ #add the counter bore
+ cbore = Solid.makeCylinder(cboreDiameter/2.0,cboreDepth,center,boreDir)
+ r = hole.fuse(cbore)
+ return r
+
+ return self.cutEach(_makeCbore,True)
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def cskHole(self,diameter, cskDiameter,cskAngle,depth=None):
+ """
+ Makes a countersunk hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param cskDiameter: the diameter of the countersink
+ :type cskDiameter: float > 0 and > diameter
+ :param cskAngle: angle of the countersink, in degrees ( 82 is common )
+ :type cskAngle: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().cskHole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` to make counterbores instead of countersinks
+ """
+
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCsk(center):
+ #center is in local coordinates
+
+ boreDir = Vector(0,0,-1)
+
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coords!
+ r = cskDiameter / 2.0
+ h = r / math.tan(math.radians(cskAngle / 2.0))
+ csk = Solid.makeCone(r,0.0,h,center,boreDir)
+ r = hole.fuse(csk)
+ return r
+
+ return self.cutEach(_makeCsk,True)
+
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def hole(self,diameter,depth=None):
+ """
+ Makes a hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().hole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` and :py:meth:`cskHole` to make counterbores or countersinks
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeHole(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0,0,-1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
+ return hole
+
+ return self.cutEach(_makeHole,True)
+
+ #TODO: duplicated code with _extrude and extrude
+ 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
+
+ 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.
+
+ **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,[])
+
+ self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+ #one would think that fusing faces into a compound and then extruding would work,
+ #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
+ #but then cutting it from the main solid fails with BRep_NotDone.
+ #the work around is to extrude each and then join the resulting solids, which seems to work
+
+ #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
+ r = None
+ for ws in wireSets:
+ thisObj = Solid.extrudeLinearWithRotation(ws[0],ws[1:],self.plane.origin, eDir,angleDegrees)
+ if r is None:
+ r = thisObj
+ else:
+ r = r.fuse(thisObj)
+
+ if combine:
+ return self._combineWithBase(r)
+ else:
+ return self.newObject([r])
+
+ def extrude(self,distance,combine=True):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic solid.
+
+ :param distance: the distance to extrude, normal to the workplane plane
+ :type distance: float, negative means opposite the normal direction
+ :param boolean combine: True to combine the resulting solid with parent solids if found.
+ :return: a CQ object with the resulting solid selected.
+
+ extrude always *adds* material to a part.
+
+ The returned object is always a CQ object, and depends on wither combine is True, and
+ whether a context solid is already defined:
+
+ * if combine is False, the new value is pushed onto the stack.
+ * if combine is true, the value is combined with the context solid if it exists,
+ and the resulting solid becomes the new context solid.
+
+ FutureEnhancement:
+ Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane
+ extrude to surface. this is quite tricky since the surface selected may not be planar
+ """
+ r = self._extrude(distance) #returns a Solid ( or a compound if there were multiple )
+ if combine:
+ return self._combineWithBase(r)
+ else:
+ return self.newObject([r])
+
+ def _combineWithBase2(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
+
+ """
+ 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 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!
+
+ :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)
+ for ss in items:
+ s = s.fuse(ss)
+
+ return self.newObject([s])
+
+ 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
+
+
+ :param toUnion:
+ :type toUnion: a solid object, or a CQ object having a solid,
+ :raises: ValueError if there is no solid to add to in the chain
+ :return: a CQ object with the resulting object selected
+ """
+
+ #first collect all of the items together
+ if type(toUnion) == CQ or type(toUnion) == Workplane:
+ solids = toUnion.solids().vals()
+ 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:
+ newS = newS.fuse(s)
+ elif type(toUnion) == Solid:
+ newS = toUnion
+ else:
+ 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
+ if combine and solidRef is not None:
+ t = solidRef.fuse(newS)
+ solidRef.wrapped = newS.wrapped
+ return self.newObject([t])
+ else:
+ return self.newObject([newS])
+
+ def cut(self,toCut,combine=True):
+ """
+ Cuts the provided solid from the current solid, IE, perform a solid subtraction
+
+ if combine=True, the result and the original are updated to point to the new object
+ if combine=False, the result will be on the stack, but the original is unmodified
+
+ :param toCut: object to cut
+ :type toCut: a solid object, or a CQ object having a solid,
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ """
+
+ solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from
+
+ if solidRef is None:
+ raise ValueError("Cannot find solid to cut from!!!")
+ solidToCut = None
+ if type(toCut) == CQ or type(toCut) == Workplane:
+ solidToCut = toCut.val()
+ elif type(toCut) == Solid:
+ solidToCut = toCut
+ else:
+ raise ValueError("Cannot cut Type '%s' " % str(type(toCut)))
+
+ newS = solidRef.cut(solidToCut)
+ if combine:
+ solidRef.wrapped = newS.wrapped
+ return self.newObject([newS])
+
+
+ def cutBlind(self,distanceToCut):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material from.
+ cutBlind always removes material from a part.
+
+ :param distanceToCut: distance to extrude before cutting
+ :type distanceToCut: float, >0 means in the positive direction of the workplane normal, <0 means in the negative direction
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutThruAll` to cut material from the entire part
+
+ Future Enhancements:
+ Cut Up to Surface
+ """
+ #first, make the object
+ toCut = self._extrude(distanceToCut)
+
+ #now find a solid in the chain
+
+ solidRef = self.findSolid()
+
+ s= solidRef.cut(toCut)
+ solidRef.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def cutThruAll(self,positive=False):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material from.
+ cutThruAll always removes material from a part.
+
+ :param boolean positive: True to cut in the positive direction, false to cut in the negative direction
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutBlind` to cut material to a limited depth
+
+ """
+ maxDim = self.largestDimension()
+ if not positive:
+ maxDim *= (-1.0)
+
+ return self.cutBlind(maxDim)
+
+
+ def loft(self,filled=True,combine=True):
+ """
+ Make a lofted solid, through the set of wires.
+ :return:
+ """
+ wiresToLoft = self.ctx.pendingWires
+ self.ctx.pendingWires = []
+
+ r = Solid.makeLoft(wiresToLoft)
+
+ if combine:
+ parentSolid = self.findSolid(searchStack=False,searchParents=True)
+ if parentSolid is not None:
+ r = parentSolid.fuse(r)
+ parentSolid.wrapped = r.wrapped
+
+ return self.newObject([r])
+
+ def _extrude(self,distance):
+ """
+ Make a prismatic solid from the existing set of pending wires.
+
+ :param distance: distance to extrude
+ :return: a FreeCAD solid, suitable for boolean operations.
+
+ This method is a utility method, primarily for plugin and internal use.
+ It is the basis for cutBlind,extrude,cutThruAll, and all similar methods.
+
+ Future Enhancements:
+ extrude along a profile ( sweep )
+ """
+
+ #group wires together into faces based on which ones are inside the others
+ #result is a list of lists
+ s = time.time()
+ 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
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+
+ #one would think that fusing faces into a compound and then extruding would work,
+ #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
+ #but then cutting it from the main solid fails with BRep_NotDone.
+ #the work around is to extrude each and then join the resulting solids, which seems to work
+
+ #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
+
+ # 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:
+ thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
+ toFuse.append(thisObj)
+
+ return Compound.makeCompound(toFuse)
+
+
+ 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.
+
+ :param length: box size in X direction
+ :type length: float > 0
+ :param width: box size in Y direction
+ :type width: float > 0
+ :param height: box size in Z direction
+ :type height: float > 0
+ :param centered: should the box be centered, or should reference point be at the lower bound of the range?
+ :param combine: should the results be combined with other solids on the stack ( and each other)?
+ :type combine: true to combine shapes, false otherwise.
+
+ Centered is a tuple that describes whether the box should be centered on the x,y, and z axes. If true,
+ the box is centered on the respective axis relative to the workplane origin, if false, the workplane center
+ will represent the lower bound of the resulting box
+
+ one box is created for each item on the current stack. If no items are on the stack, one box using
+ the current workplane center is created.
+
+ If combine is true, the result will be a single object on the stack:
+ if a solid was found in the chain, the result is that solid with all boxes produced fused onto it
+ otherwise, the result is the combination of all the produced boxes
+
+ if combine is false, the result will be a list of the boxes produced
+
+ Most often boxes form the basis for a part::
+
+ #make a single box with lower left corner at origin
+ s = Workplane().box(1,2,3,centered=(False,False,False)
+
+ But sometimes it is useful to create an array of them:
+
+ #create 4 small square bumps on a larger base plate:
+ s = Workplane().box(4,4,0.5).faces(">Z").workplane()\
+ .rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True)
+
+ """
+
+ def _makebox(pnt):
+
+ #(xp,yp,zp) = self.plane.toLocalCoords(pnt)
+ (xp,yp,zp) = pnt.toTuple()
+ if centered[0]:
+ xp = xp-(length/2.0)
+ if centered[1]:
+ yp = yp-(width/2.0)
+ if centered[2]:
+ zp = zp-(height/2.0)
+
+ return Solid.makeBox(length,width,height,Vector(xp,yp,zp))
+
+ boxes = self.eachpoint(_makebox,True)
+
+ #if combination is not desired, just return the created boxes
+ if not combine:
+ return boxes
+ else:
+ #combine everything
+ return self.union(boxes)