diff --git a/build/lib.linux-i686-2.7/cadquery/__init__.py b/build/lib.linux-i686-2.7/cadquery/__init__.py
new file mode 100644
index 0000000..05f7120
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/__init__.py
@@ -0,0 +1,21 @@
+#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 *
+from .cq import *
+
+
+__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.3.0"
diff --git a/build/lib.linux-i686-2.7/cadquery/contrib/__init__.py b/build/lib.linux-i686-2.7/cadquery/contrib/__init__.py
new file mode 100644
index 0000000..67c7b68
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/contrib/__init__.py
@@ -0,0 +1,18 @@
+"""
+ Copyright (C) 2011-2015 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/build/lib.linux-i686-2.7/cadquery/cq.py b/build/lib.linux-i686-2.7/cadquery/cq.py
new file mode 100644
index 0000000..59a1180
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/cq.py
@@ -0,0 +1,2467 @@
+"""
+ Copyright (C) 2011-2015 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
+import math
+from cadquery import *
+from cadquery import selectors
+from cadquery import exporters
+
+
+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 created pending edges that need to be joined into wires
+ # a reference to the first point for a set of edges.
+ # Used to determine how to behave when close() is called
+ self.firstPoint = None
+ self.tolerance = 0.0001 # user specified tolerance
+
+
+class CQ(object):
+ """
+ Provides enhanced functionality for a wrapped CAD primitive.
+
+ Examples include feature selection, feature creation, 2d drawing
+ using work planes, and 3d operations 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
+ :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 correctly,
+ so hashCode is used to ensure we don't add the same
+ object multiple times.
+
+ One weird use case is that the stack could have a solid reference object
+ on it. This is meant to be a reference to the most recently modified version
+ of the context solid, whatever it is.
+ """
+ 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 solidto 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 and we don't want to find our own objects
+ ctxSolid = self.findSolid(searchStack=False, searchParents=True)
+
+ if ctxSolid is None:
+ ctxSolid = toCombine.pop(0)
+
+ #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
+ """
+ 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, centerOption='CenterOfMass'):
+ """
+ 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, a set of
+ co-planar faces 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/faces, if a face/faces 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.
+ """
+ def _isCoPlanar(f0, f1):
+ """Test if two faces are on the same plane."""
+ p0 = f0.Center()
+ p1 = f1.Center()
+ n0 = f0.normalAt()
+ n1 = f1.normalAt()
+
+ # test normals (direction of planes)
+ if not ((abs(n0.x-n1.x) < self.ctx.tolerance) or
+ (abs(n0.y-n1.y) < self.ctx.tolerance) or
+ (abs(n0.z-n1.z) < self.ctx.tolerance)):
+ return False
+
+ # test if p1 is on the plane of f0 (offset of planes)
+ return abs(n0.dot(p0.sub(p1)) < self.ctx.tolerance)
+
+ def _computeXdir(normal):
+ """
+ Figures out the X direction based on the given normal.
+ :param :normal The direction that's normal to the plane.
+ :type :normal A Vector
+ :return A vector representing the X direction.
+ """
+ xd = Vector(0, 0, 1).cross(normal)
+ if xd.Length < self.ctx.tolerance:
+ #this face is parallel with the x-y plane, so choose x to be in global coordinates
+ xd = Vector(1, 0, 0)
+ return xd
+
+ if len(self.objects) > 1:
+ # are all objects 'PLANE'?
+ if not all(o.geomType() == 'PLANE' for o in self.objects):
+ raise ValueError("If multiple objects selected, they all must be planar faces.")
+
+ # are all faces co-planar with each other?
+ if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]):
+ raise ValueError("Selected faces must be co-planar.")
+
+ if centerOption == 'CenterOfMass':
+ center = Shape.CombinedCenter(self.objects)
+ elif centerOption == 'CenterOfBoundBox':
+ center = Shape.CombinedCenterOfBoundBox(self.objects)
+
+ normal = self.objects[0].normalAt()
+ xDir = _computeXdir(normal)
+
+ else:
+ obj = self.objects[0]
+
+ if isinstance(obj, Face):
+ if centerOption == 'CenterOfMass':
+ center = obj.Center()
+ elif centerOption == 'CenterOfBoundBox':
+ center = obj.CenterOfBoundBox()
+ normal = obj.normalAt(center)
+ xDir = _computeXdir(normal)
+ else:
+ if hasattr(obj, 'Center'):
+ if centerOption == 'CenterOfMass':
+ center = obj.Center()
+ elif centerOption == 'CenterOfBoundBox':
+ center = obj.CenterOfBoundBox()
+ 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.normalized().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 isinstance(s, Solid):
+ return s
+ elif isinstance(s, Compound):
+ return s.Solids()
+
+ 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 implementation of the vertices,edges,faces,
+ solids,shells, and other similar selector methods. It is a useful extension point for
+ plugin developers to make other selector methods.
+ """
+ # A single list of all faces from all objects on the stack
+ toReturn = self._collectProperty(objType)
+
+ if selector is not None:
+ if isinstance(selector, str) or isinstance(selector, unicode):
+ 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 who's stack contains the *distinct* vertices of *all* objects on the
+ current stack, after being filtered by the selector, if provided
+
+ If there are no vertices for any objects on the current stack, an empty CQ object
+ is returned
+
+ 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 who's stack contains all of the *distinct* faces of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no vertices for any objects on the current stack, an empty CQ object
+ is returned.
+
+ 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 who's stack contains all of the *distinct* edges of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no edges for any objects on the current stack, an empty CQ object is returned
+
+ 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 who's stack contains all of the *distinct* wires of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no wires for any objects on the current stack, an empty CQ object is returned
+
+ 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 who's stack contains all of the *distinct* solids of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no solids for any objects on the current stack, an empty CQ object is returned
+
+ 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 who's stack contains all of the *distinct* solids of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ If there are no shells for any objects on the current stack, an empty CQ object is returned
+
+ Most solids will have a single shell, which represents the outer surface. A shell will
+ typically be composed of multiple faces.
+
+ 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 who's stack contains all of the *distinct* solids of *all* objects on
+ the current stack, filtered by the provided selector.
+
+ A compound contains multiple CAD primitives that resulted from a single operation, such as
+ a union, cut, split, or fillet. Compounds can contain multiple edges, wires, or solids.
+
+ 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 opts: svg formatting options
+ :type opts: dictionary, width and height
+ :return: a string that contains SVG that represents this item.
+ """
+ return exporters.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, because the axis of rotation
+ could be inconsistent between multiple objects. This is because the beginning
+ of the axis is variable, while the end is fixed. This is fine when operating on
+ one object, but is not cool for multiple.
+ """
+
+ #center point is the first point in the vector
+ endVec = Vector(axisEndPoint)
+
+ def _rot(obj):
+ startPt = obj.Center()
+ endPt = startPt + endVec
+ return obj.rotate(startPt, endPt, angleDegrees)
+
+ return self.each(_rot, False)
+
+ def rotate(self, axisStartPoint, axisEndPoint, angleDegrees):
+ """
+ Returns a copy of all of the items on the stack rotated through and angle around the axis
+ of rotation.
+
+ :param axisStartPoint: The first point of the axis of rotation
+ :type axisStartPoint: a 3-tuple of floats
+ :type axisEndPoint: The second point of the axis of rotation
+ :type axisEndPoint: a 3-tuple of floats
+ :param angleDegrees: the rotation angle, in degrees
+ :type angleDegrees: float
+ :returns: a CQ object
+ """
+ return self.newObject([o.rotate(axisStartPoint, axisEndPoint, angleDegrees)
+ for o in self.objects])
+
+ 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
+ """
+ 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,
+ # TODO: we 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])
+
+ def chamfer(self, length, length2=None):
+ """
+ Chamfers a solid on the selected edges.
+
+ The edges on the stack are chamfered. The solid to which the
+ edges belong must be in the parent chain of the selected
+ edges.
+
+ Optional parameter `length2` can be supplied with a different
+ value than `length` for a chamfer that is shorter on one side
+ longer on the other side.
+
+ :param length: the length of the fillet, must be greater than zero
+ :param length2: optional parameter for asymmetrical chamfer
+ :type length: positive float
+ :type length2: 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 chamfered::
+
+ s = Workplane("XY").box(1,1,1).faces("+Z").chamfer(0.1)
+
+ This example will create chamfers longer on the sides::
+
+ s = Workplane("XY").box(1,1,1).faces("+Z").chamfer(0.2, 0.1)
+ """
+ solid = self.findSolid()
+
+ edgeList = self.edges().vals()
+ if len(edgeList) < 1:
+ raise ValueError("Chamfer requires that edges be selected")
+
+ s = solid.chamfer(length, length2, 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 inPlane: the plane in which the workplane will be done
+ :type inPlane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial
+ stack value.
+ :raises: ValueError if the provided plane is not a plane, 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 isinstance(inPlane, str) or isinstance(inPlane, unicode):
+ tmpPlane = Plane.named(inPlane, origin)
+ else:
+ tmpPlane = None
+
+ if tmpPlane is None:
+ raise ValueError(
+ 'Provided value {} is not a valid work plane'.format(inPlane))
+
+ self.obj = obj
+ self.plane = tmpPlane
+ self.firstPoint = None
+ # Changed so that workplane has the center as the first item on the stack
+ self.objects = [self.plane.origin]
+ self.parent = None
+ self.ctx = CQContext()
+
+ def transformed(self, rotate=(0, 0, 0), offset=(0, 0, 0)):
+ """
+ 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.origin = 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 last object on the stack is used.
+
+ NOTE:
+ """
+ obj = self.objects[-1]
+
+ 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 points ( > 0 )
+ :param center: if true, the array will be centered at the center of the workplane. if
+ false, the lower left corner will be at the center of the work plane
+ """
+
+ if xSpacing < 1 or ySpacing < 1 or xCount < 1 or yCount < 1:
+ raise ValueError("Spacing and count must be > 0 ")
+
+ lpoints = [] # coordinates relative to bottom left point
+ 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 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)
+
+ def vLineTo(self, yCoord, forConstruction=False):
+ """
+ Make a vertical line from the current point to the provided y coordinate.
+
+ Useful if it is more convenient to specify the end location rather than distance,
+ as in :py:meth:`vLine`
+
+ :param float yCoord: y coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(p.x, yCoord, forConstruction)
+
+ def hLineTo(self, xCoord, forConstruction=False):
+ """
+ Make a horizontal line from the current point to the provided x coordinate.
+
+ Useful if it is more convenient to specify the end location rather than distance,
+ as in :py:meth:`hLine`
+
+ :param float xCoord: x coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(xCoord, p.y, forConstruction)
+
+ #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 coordinates
+ :type y: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`move` to do the same thing but using relative dimensions
+ """
+ newCenter = Vector(x, y, 0)
+ return self.newObject([self.plane.toWorldCoords(newCenter)])
+
+ #relative move in current plane, not drawing
+ def move(self, xDist=0, yDist=0):
+ """
+ Move the specified distance from the current point, without drawing.
+
+ :param xDist: desired x distance, in local coordinates
+ :type xDist: float, or none for zero
+ :param yDist: desired y distance, in local coordinates
+ :type yDist: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ 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.toWorldCoords(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 tuple 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 Enhancements:
+ provide a version that allows an arc using relative measures
+ provide a centerpoint arc
+ provide tangent arcs
+ """
+
+ gstartPoint = self._findFromPoint(False)
+ gpoint1 = self.plane.toWorldCoords(point1)
+ gpoint2 = self.plane.toWorldCoords(point2)
+
+ arc = Edge.makeThreePointArc(gstartPoint, gpoint1, gpoint2)
+
+ 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 = self.plane.toLocalCoords(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 automatically combines them into faces
+ when necessary to make a solid.
+ """
+ self.ctx.pendingWires.append(wire)
+
+ def consolidateWires(self):
+ """
+ Attempt to consolidate wires on the stack into a single.
+ If possible, a new object with the results are returned.
+ if not possible, the wires remain separated
+
+ FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly sometimes
+ Additionally, it has a bug where a profile composed of two wires ( rather than one )
+ also does not work properly. Together these are a real problem.
+ """
+ wires = self.wires().vals()
+ if len(wires) < 2:
+ 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 utilities for 2-d construction.
+ This method should be called when a user operation implies that 2-d construction is
+ finished, and we are ready to begin working in 3d
+
+ SEE '2-d construction concepts' for a more detailed explanation of how CadQuery handles
+ edges, wires, etc
+
+ 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 coordinates after they are returned.
+
+ This allows plugin developers to create objects in local coordinates, without worrying
+ about the fact that the working plane is different than the global coordinate system.
+
+
+ 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)
+
+ 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, forConstruction=False):
+ """
+ Creates a polygon inscribed in a circle of the specified diameter for each point on
+ the stack
+
+ The first vertex is always oriented in the x direction.
+
+ :param nSides: number of sides, must be > 3
+ :param diameter: the size of the circle the polygon is inscribed 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, forConstruction)
+
+ 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: whether or not the edges are used for reference
+ :type forConstruction: true if the edges are for reference, false if they are for creating geometry
+ part geometry
+ :return: a new CQ object with a list of edges on the stack
+
+ *NOTE* most commonly, the resulting wire should be closed.
+ """
+
+ # Our list of new edges that will go into a new CQ object
+ edges = []
+
+ # The very first startPoint comes from our original object, but not after that
+ startPoint = self._findFromPoint(False)
+
+ # Draw a line for each set of points, starting from the from-point of the original CQ object
+ for curTuple in listOfXYTuple:
+ endPoint = self.plane.toWorldCoords(curTuple)
+
+ edges.append(Edge.makeLine(startPoint, endPoint))
+
+ # We need to move the start point for the next line that we draw or we get stuck at the same startPoint
+ startPoint = endPoint
+
+ if not forConstruction:
+ self._addPendingEdge(edges[-1])
+
+ return self.newObject(edges)
+
+ 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)
+
+ # Need to reset the first point after closing a wire
+ self.ctx.firstPoint=None
+
+ 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: A value representing the largest dimension of the first solid on the stack
+ """
+ #TODO: this implementation is naive and returns the dims of the first solid... most of
+ #TODO: the time this works. but a stronger implementation would be to search all solids.
+ s = self.findSolid()
+ if s:
+ return s.BoundingBox().DiagonalLength * 5.0
+ else:
+ return -1
+
+ def cutEach(self, fcn, useLocalCoords=False, clean=True):
+ """
+ Evaluates the provided function at each point on the stack (ie, eachpoint)
+ and then cuts the result from the context solid.
+ :param fcn: a function suitable for use in the eachpoint method: ie, that accepts a vector
+ :param useLocalCoords: same as for :py:meth:`eachpoint`
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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)
+
+ if clean: s = s.clean()
+
+ 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, clean=True):
+ """
+ Makes a counterbored hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diameter: float > 0
+ :param cboreDiameter: the diameter of the cbore
+ :type cboreDiameter: float > 0 and > diameter
+ :param cboreDepth: depth of the counterbore
+ :type cboreDepth: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ 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, clean)
+
+ #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, clean=True):
+ """
+ Makes a countersunk hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diameter: float > 0
+ :param cskDiameter: the diameter of the countersink
+ :type cskDiameter: float > 0 and > diameter
+ :param cskAngle: angle of the countersink, in degrees ( 82 is common )
+ :type cskAngle: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ 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, clean)
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def hole(self, diameter, depth=None, clean=True):
+ """
+ Makes a hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diameter: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ 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 coordinates!
+ return hole
+
+ return self.cutEach(_makeHole, True, clean)
+
+ #TODO: duplicated code with _extrude and extrude
+ def twistExtrude(self, distance, angleDegrees, combine=True, clean=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.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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:
+ newS = self._combineWithBase(r)
+ else:
+ newS = self.newObject([r])
+ if clean: newS = newS.clean()
+ return newS
+
+ def extrude(self, distance, combine=True, clean=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.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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:
+ newS = self._combineWithBase(r)
+ else:
+ newS = self.newObject([r])
+ if clean: newS = newS.clean()
+ return newS
+
+ def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=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
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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 %= 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()
+
+ # returns a Solid (or a compound if there were multiple)
+ r = self._revolve(angleDegrees, axisStart, axisEnd)
+ if combine:
+ newS = self._combineWithBase(r)
+ else:
+ newS = self.newObject([r])
+ if clean: newS = newS.clean()
+ return newS
+
+ 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, clean=True):
+ """
+ Attempts to combine all of the items on the stack into a single item.
+ WARNING: all of the items must be of the same type!
+
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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)
+
+ if clean: s = s.clean()
+
+ return self.newObject([s])
+
+ def union(self, toUnion=None, combine=True, clean=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,
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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 '{}'".format(type(toUnion)))
+
+ #now combine with existing solid, if there is one
+ # look for parents to cut from
+ solidRef = self.findSolid(searchStack=True, searchParents=True)
+ if combine and solidRef is not None:
+ r = solidRef.fuse(newS)
+ solidRef.wrapped = newS.wrapped
+ else:
+ r = newS
+
+ if clean: r = r.clean()
+
+ return self.newObject([r])
+
+ def cut(self, toCut, combine=True, clean=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,
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+ """
+
+ # look for parents to cut from
+ solidRef = self.findSolid(searchStack=True, searchParents=True)
+
+ if solidRef is None:
+ raise ValueError("Cannot find solid to cut from")
+ 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 '{}'".format(type(toCut)))
+
+ newS = solidRef.cut(solidToCut)
+
+ if clean: newS = newS.clean()
+
+ if combine:
+ solidRef.wrapped = newS.wrapped
+
+ return self.newObject([newS])
+
+ def cutBlind(self, distanceToCut, clean=True):
+ """
+ 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
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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)
+
+ if clean: s = s.clean()
+
+ solidRef.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def cutThruAll(self, positive=False, clean=True):
+ """
+ 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
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+ :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, clean)
+
+ def loft(self, filled=True, ruled=False, combine=True):
+ """
+ Make a lofted solid, through the set of wires.
+ :return: a CQ object containing the created loft
+ """
+ wiresToLoft = self.ctx.pendingWires
+ self.ctx.pendingWires = []
+
+ r = Solid.makeLoft(wiresToLoft, ruled)
+
+ 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, clean=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.
+ :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
+
+ 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 -= (length / 2.0)
+ if centered[1]:
+ yp -= (width / 2.0)
+ if centered[2]:
+ 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, clean=clean)
+
+ def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360,
+ centered=(True, True, True), combine=True, clean=True):
+ """
+ Returns a 3D sphere with the specified radius for each point on the stack
+
+ :param radius: The radius of the sphere
+ :type radius: float > 0
+ :param direct: The direction axis for the creation of the sphere
+ :type direct: A three-tuple
+ :param angle1: The first angle to sweep the sphere arc through
+ :type angle1: float > 0
+ :param angle2: The second angle to sweep the sphere arc through
+ :type angle2: float > 0
+ :param angle3: The third angle to sweep the sphere arc through
+ :type angle3: float > 0
+ :param centered: A three-tuple of booleans that determines whether the sphere is centered
+ on each axis origin
+ :param combine: Whether the results should be combined with other solids on the stack
+ (and each other)
+ :type combine: true to combine shapes, false otherwise
+ :return: A sphere object for each point on the stack
+
+ Centered is a tuple that describes whether the sphere should be centered on the x,y, and
+ z axes. If true, the sphere is centered on the respective axis relative to the workplane
+ origin, if false, the workplane center will represent the lower bound of the resulting
+ sphere.
+
+ One sphere is created for each item on the current stack. If no items are on the stack, one
+ box using the current workplane center is created.
+
+ If combine is true, the result will be a single object on the stack:
+ If a solid was found in the chain, the result is that solid with all spheres produced
+ fused onto it otherwise, the result is the combination of all the produced boxes
+
+ If combine is false, the result will be a list of the spheres produced
+ """
+
+ # Convert the direction tuple to a vector, if needed
+ if isinstance(direct, tuple):
+ direct = Vector(direct)
+
+ def _makesphere(pnt):
+ """
+ Inner function that is used to create a sphere for each point/object on the workplane
+ :param pnt: The center point for the sphere
+ :return: A CQ Solid object representing a sphere
+ """
+ (xp, yp, zp) = pnt.toTuple()
+
+ if not centered[0]:
+ xp += radius
+
+ if not centered[1]:
+ yp += radius
+
+ if not centered[2]:
+ zp += radius
+
+ return Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3)
+
+ # We want a sphere for each point on the workplane
+ spheres = self.eachpoint(_makesphere, True)
+
+ # If we don't need to combine everything, just return the created spheres
+ if not combine:
+ return spheres
+ else:
+ return self.union(spheres, clean=clean)
+
+ def clean(self):
+ """
+ Cleans the current solid by removing unwanted edges from the
+ faces.
+
+ Normally you don't have to call this function. It is
+ automatically called after each related operation. You can
+ disable this behavior with `clean=False` parameter if method
+ has any. In some cases this can improve performance
+ drastically but is generally dis-advised since it may break
+ some operations such as fillet.
+
+ Note that in some cases where lots of solid operations are
+ chained, `clean()` may actually improve performance since
+ the shape is 'simplified' at each step and thus next operation
+ is easier.
+
+ Also note that, due to limitation of the underlying engine,
+ `clean` may fail to produce a clean output in some cases such as
+ spherical faces.
+ """
+ try:
+ cleanObjects = [obj.clean() for obj in self.objects]
+ except AttributeError:
+ raise AttributeError("%s object doesn't support `clean()` method!" % obj.ShapeType())
+ return self.newObject(cleanObjects)
diff --git a/build/lib.linux-i686-2.7/cadquery/cq_directive.py b/build/lib.linux-i686-2.7/cadquery/cq_directive.py
new file mode 100644
index 0000000..0dc5fae
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/cq_directive.py
@@ -0,0 +1,85 @@
+"""
+A special directive for including a cq object.
+
+"""
+
+import traceback
+from cadquery import *
+from cadquery import cqgi
+import StringIO
+from docutils.parsers.rst import directives
+
+template = """
+
+.. raw:: html
+
+
+ %(out_svg)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
+ out_svg = "Your Script Did not assign call build_output() function!"
+
+ try:
+ _s = StringIO.StringIO()
+ result = cqgi.parse(plot_code).build()
+
+ if result.success:
+ exporters.exportShape(result.first_result, "SVG", _s)
+ out_svg = _s.getvalue()
+ else:
+ raise result.exception
+
+ except Exception:
+ traceback.print_exc()
+ out_svg = traceback.format_exc()
+
+ # now out
+ # Now start generating the lines of output
+ lines = []
+
+ # get rid of new lines
+ out_svg = out_svg.replace('\n', '')
+
+ txt_align = "left"
+ if "align" in options:
+ txt_align = 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/build/lib.linux-i686-2.7/cadquery/cqgi.py b/build/lib.linux-i686-2.7/cadquery/cqgi.py
new file mode 100644
index 0000000..92fd860
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/cqgi.py
@@ -0,0 +1,425 @@
+"""
+The CadQuery Gateway Interface.
+Provides classes and tools for executing CadQuery scripts
+"""
+import ast
+import traceback
+import time
+import cadquery
+
+CQSCRIPT = ""
+
+def parse(script_source):
+ """
+ Parses the script as a model, and returns a model.
+
+ If you would prefer to access the underlying model without building it,
+ for example, to inspect its available parameters, construct a CQModel object.
+
+ :param script_source: the script to run. Must be a valid cadquery script
+ :return: a CQModel object that defines the script and allows execution
+
+ """
+ model = CQModel(script_source)
+ return model
+
+
+class CQModel(object):
+ """
+ Represents a Cadquery Script.
+
+ After construction, the metadata property contains
+ a ScriptMetaData object, which describes the model in more detail,
+ and can be used to retrive the parameters defined by the model.
+
+ the build method can be used to generate a 3d model
+ """
+
+ def __init__(self, script_source):
+ """
+ Create an object by parsing the supplied python script.
+ :param script_source: a python script to parse
+ """
+ self.metadata = ScriptMetadata()
+ self.ast_tree = ast.parse(script_source, CQSCRIPT)
+ self.script_source = script_source
+ self._find_vars()
+ # TODO: pick up other scirpt metadata:
+ # describe
+ # pick up validation methods
+
+ def _find_vars(self):
+ """
+ Parse the script, and populate variables that appear to be
+ overridable.
+ """
+ #assumption here: we assume that variable declarations
+ #are only at the top level of the script. IE, we'll ignore any
+ #variable definitions at lower levels of the script
+
+ #we dont want to use the visit interface because here we excplicitly
+ #want to walk only the top level of the tree.
+ assignment_finder = ConstantAssignmentFinder(self.metadata)
+
+ for node in self.ast_tree.body:
+ if isinstance(node, ast.Assign):
+ assignment_finder.visit_Assign(node)
+
+
+ def validate(self, params):
+ """
+ Determine if the supplied parameters are valid.
+ NOT IMPLEMENTED YET-- raises NotImplementedError
+ :param params: a dictionary of parameters
+
+ """
+ raise NotImplementedError("not yet implemented")
+
+ def build(self, build_parameters=None):
+ """
+ Executes the script, using the optional parameters to override those in the model
+ :param build_parameters: a dictionary of variables. The variables must be
+ assignable to the underlying variable type.
+ :raises: Nothing. If there is an exception, it will be on the exception property of the result.
+ This is the interface so that we can return other information on the result, such as the build time
+ :return: a BuildResult object, which includes the status of the result, and either
+ a resulting shape or an exception
+ """
+ if not build_parameters:
+ build_parameters = {}
+
+ start = time.clock()
+ result = BuildResult()
+
+ try:
+ self.set_param_values(build_parameters)
+ collector = ScriptCallback()
+ env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
+ .add_entry("build_object", collector.build_object).build()
+
+ c = compile(self.ast_tree, CQSCRIPT, 'exec')
+ exec (c, env)
+ if collector.has_results():
+ result.set_success_result(collector.outputObjects)
+ else:
+ raise NoOutputError("Script did not call build_object-- no output available.")
+ except Exception, ex:
+ print "Error Executing Script:"
+ result.set_failure_result(ex)
+ traceback.print_exc()
+ print "Full Text of Script:"
+ print self.script_source
+
+ end = time.clock()
+ result.buildTime = end - start
+ return result
+
+ def set_param_values(self, params):
+ model_parameters = self.metadata.parameters
+
+ for k, v in params.iteritems():
+ if k not in model_parameters:
+ raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k)
+
+ p = model_parameters[k]
+ p.set_value(v)
+
+
+class BuildResult(object):
+ """
+ The result of executing a CadQuery script.
+ The success property contains whether the exeuction was successful.
+
+ If successful, the results property contains a list of all results,
+ and the first_result property contains the first result.
+
+ If unsuccessful, the exception property contains a reference to
+ the stack trace that occurred.
+ """
+ def __init__(self):
+ self.buildTime = None
+ self.results = []
+ self.first_result = None
+ self.success = False
+ self.exception = None
+
+ def set_failure_result(self, ex):
+ self.exception = ex
+ self.success = False
+
+ def set_success_result(self, results):
+ self.results = results
+ self.first_result = self.results[0]
+ self.success = True
+
+
+class ScriptMetadata(object):
+ """
+ Defines the metadata for a parsed CQ Script.
+ the parameters property is a dict of InputParameter objects.
+ """
+ def __init__(self):
+ self.parameters = {}
+
+ def add_script_parameter(self, p):
+ self.parameters[p.name] = p
+
+
+class ParameterType(object):
+ pass
+
+
+class NumberParameterType(ParameterType):
+ pass
+
+
+class StringParameterType(ParameterType):
+ pass
+
+
+class BooleanParameterType(ParameterType):
+ pass
+
+
+class InputParameter:
+ """
+ Defines a parameter that can be supplied when the model is executed.
+
+ Name, varType, and default_value are always available, because they are computed
+ from a variable assignment line of code:
+
+ The others are only available if the script has used define_parameter() to
+ provide additional metadata
+
+ """
+ def __init__(self):
+
+ #: the default value for the variable.
+ self.default_value = None
+
+ #: the name of the parameter.
+ self.name = None
+
+ #: type of the variable: BooleanParameter, StringParameter, NumericParameter
+ self.varType = None
+
+ #: help text describing the variable. Only available if the script used describe_parameter()
+ self.shortDesc = None
+
+
+
+ #: valid values for the variable. Only available if the script used describe_parameter()
+ self.valid_values = []
+
+
+
+ self.ast_node = None
+
+ @staticmethod
+ def create(ast_node, var_name, var_type, default_value, valid_values=None, short_desc=None):
+
+ if valid_values is None:
+ valid_values = []
+
+ p = InputParameter()
+ p.ast_node = ast_node
+ p.default_value = default_value
+ p.name = var_name
+ if short_desc is None:
+ p.shortDesc = var_name
+ else:
+ p.shortDesc = short_desc
+ p.varType = var_type
+ p.valid_values = valid_values
+ return p
+
+ def set_value(self, new_value):
+
+ if len(self.valid_values) > 0 and new_value not in self.valid_values:
+ raise InvalidParameterError(
+ "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} "
+ .format(str(new_value), self.name, str(self.valid_values)))
+
+ if self.varType == NumberParameterType:
+ try:
+ f = float(new_value)
+ self.ast_node.n = f
+ except ValueError:
+ raise InvalidParameterError(
+ "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric."
+ .format(str(new_value), self.name))
+
+ elif self.varType == StringParameterType:
+ self.ast_node.s = str(new_value)
+ elif self.varType == BooleanParameterType:
+ if new_value:
+ self.ast_node.id = 'True'
+ else:
+ self.ast_node.id = 'False'
+ else:
+ raise ValueError("Unknown Type of var: ", str(self.varType))
+
+ def __str__(self):
+ return "InputParameter: {name=%s, type=%s, defaultValue=%s" % (
+ self.name, str(self.varType), str(self.default_value))
+
+
+class ScriptCallback(object):
+ """
+ Allows a script to communicate with the container
+ the build_object() method is exposed to CQ scripts, to allow them
+ to return objects to the execution environment
+ """
+
+ def __init__(self):
+ self.outputObjects = []
+
+ def build_object(self, shape):
+ """
+ return an object to the executing environment
+ :param shape: a cadquery object
+ """
+ self.outputObjects.append(shape)
+
+ def describe_parameter(self,var, valid_values, short_desc):
+ """
+ Not yet implemented: allows a script to document
+ extra metadata about the parameters
+ """
+ pass
+
+ def add_error(self, param, field_list):
+ """
+ Not implemented yet: allows scripts to indicate that there are problems with inputs
+ """
+ pass
+
+ def has_results(self):
+ return len(self.outputObjects) > 0
+
+
+
+class InvalidParameterError(Exception):
+ """
+ Raised when an attempt is made to provide a new parameter value
+ that cannot be assigned to the model
+ """
+ pass
+
+
+class NoOutputError(Exception):
+ """
+ Raised when the script does not execute the build_object() method to
+ return a solid
+ """
+ pass
+
+
+class ScriptExecutionError(Exception):
+ """
+ Represents a script syntax error.
+ Useful for helping clients pinpoint issues with the script
+ interactively
+ """
+
+ def __init__(self, line=None, message=None):
+ if line is None:
+ self.line = 0
+ else:
+ self.line = line
+
+ if message is None:
+ self.message = "Unknown Script Error"
+ else:
+ self.message = message
+
+ def full_message(self):
+ return self.__repr__()
+
+ def __str__(self):
+ return self.__repr__()
+
+ def __repr__(self):
+ return "ScriptError [Line %s]: %s" % (self.line, self.message)
+
+
+class EnvironmentBuilder(object):
+ """
+ Builds an execution environment for a cadquery script.
+ The environment includes the builtins, as well as
+ the other methods the script will need.
+ """
+ def __init__(self):
+ self.env = {}
+
+ def with_real_builtins(self):
+ return self.with_builtins(__builtins__)
+
+ def with_builtins(self, env_dict):
+ self.env['__builtins__'] = env_dict
+ return self
+
+ def with_cadquery_objects(self):
+ self.env['cadquery'] = cadquery
+ self.env['cq'] = cadquery
+ return self
+
+ def add_entry(self, name, value):
+ self.env[name] = value
+ return self
+
+ def build(self):
+ return self.env
+
+
+class ConstantAssignmentFinder(ast.NodeTransformer):
+ """
+ Visits a parse tree, and adds script parameters to the cqModel
+ """
+
+ def __init__(self, cq_model):
+ self.cqModel = cq_model
+
+ def handle_assignment(self, var_name, value_node):
+
+
+
+ try:
+
+ if type(value_node) == ast.Num:
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
+ elif type(value_node) == ast.Str:
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
+ elif type(value_node == ast.Name):
+ if value_node.id == 'True':
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, BooleanParameterType, True))
+ elif value_node.id == 'False':
+ self.cqModel.add_script_parameter(
+ InputParameter.create(value_node, var_name, BooleanParameterType, True))
+ except:
+ print "Unable to handle assignment for variable '%s'" % var_name
+ pass
+
+ def visit_Assign(self, node):
+
+ try:
+ left_side = node.targets[0]
+
+ #do not handle attribute assignments
+ if isinstance(left_side,ast.Attribute):
+ return
+
+ if type(node.value) in [ast.Num, ast.Str, ast.Name]:
+ self.handle_assignment(left_side.id, node.value)
+ elif type(node.value) == ast.Tuple:
+ # we have a multi-value assignment
+ for n, v in zip(left_side.elts, node.value.elts):
+ self.handle_assignment(n.id, v)
+ except:
+ traceback.print_exc()
+ print "Unable to handle assignment for node '%s'" % ast.dump(left_side)
+
+ return node
diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/__init__.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/__init__.py
new file mode 100644
index 0000000..3e041e3
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/__init__.py
@@ -0,0 +1,112 @@
+"""
+ Copyright (C) 2011-2015 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",
+ "/opt/freecad/lib/",
+ "/usr/bin/freecad/lib",
+ "/usr/lib/freecad",
+ ]:
+ 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
+ elif sys.platform.startswith('darwin'):
+ #Assume we're dealing with a Mac
+ for _PATH in [
+ "/Applications/FreeCAD.app/Contents/lib",
+ os.path.join(os.path.expanduser("~"), "Library/Application Support/FreeCAD/lib"),
+ ]:
+ if os.path.exists(_PATH):
+ return _PATH
+
+
+
+#Make sure that the correct FreeCAD path shows up in Python's system path
+no_library_path = ImportError('cadquery was unable to determine freecads library path')
+try:
+ import FreeCAD
+except ImportError:
+ path = _fc_path()
+ if path:
+ sys.path.insert(0, path)
+ try:
+ import FreeCAD
+ except ImportError:
+ raise no_library_path
+ else: raise no_library_path
diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/exporters.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/exporters.py
new file mode 100644
index 0000000..c4b097a
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/exporters.py
@@ -0,0 +1,392 @@
+import cadquery
+
+import FreeCAD
+import Drawing
+
+import tempfile, os, StringIO
+
+
+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/build/lib.linux-i686-2.7/cadquery/freecad_impl/geom.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/geom.py
new file mode 100644
index 0000000..2bd8c3a
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/geom.py
@@ -0,0 +1,647 @@
+"""
+ Copyright (C) 2011-2015 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
+import cadquery
+import FreeCAD
+import Part as FreeCADPart
+
+
+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:
+ * nothing (in which case the null vector is return)
+ * a FreeCAD vector
+ * a vector ( in which case it is copied )
+ * a 3-tuple
+ * three float values, x, y, and z
+ """
+ def __init__(self, *args):
+ if len(args) == 3:
+ fV = FreeCAD.Base.Vector(args[0], args[1], args[2])
+ elif len(args) == 1:
+ if isinstance(args[0], Vector):
+ fV = args[0].wrapped
+ elif isinstance(args[0], tuple):
+ fV = FreeCAD.Base.Vector(args[0][0], args[0][1], args[0][2])
+ elif isinstance(args[0], FreeCAD.Base.Vector):
+ fV = args[0]
+ else:
+ fV = args[0]
+ elif len(args) == 0:
+ fV = FreeCAD.Base.Vector(0, 0, 0)
+ else:
+ raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple")
+
+ self._wrapped = fV
+
+ @property
+ def x(self):
+ return self.wrapped.x
+
+ @property
+ def y(self):
+ return self.wrapped.y
+
+ @property
+ def z(self):
+ return self.wrapped.z
+
+ @property
+ def Length(self):
+ return self.wrapped.Length
+
+ @property
+ def wrapped(self):
+ return self._wrapped
+
+ 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 Vector(self.wrapped.sub(v.wrapped))
+
+ def add(self, v):
+ return Vector(self.wrapped.add(v.wrapped))
+
+ def multiply(self, scale):
+ """Return a copy multiplied by the provided scalar"""
+ tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
+ return Vector(tmp_fc_vector.multiply(scale))
+
+ def normalized(self):
+ """Return a normalized version of this vector"""
+ tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped)
+ tmp_fc_vector.normalize()
+ return Vector(tmp_fc_vector)
+
+ def Center(self):
+ """Return the vector itself
+
+ 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 __add__(self, v):
+ return self.add(v)
+
+ def __repr__(self):
+ return self.wrapped.__repr__()
+
+ def __str__(self):
+ return self.wrapped.__str__()
+
+ def __ne__(self, other):
+ return self.wrapped.__ne__(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 is 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(object):
+ """A 2D coordinate system in space
+
+ A 2D coordinate system in space, with the x-y axes on the 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|ZX|XZ|YX|ZY|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
+ ZX +z +x +y
+ XZ +x +z -y
+ YX +y +x -z
+ ZY +z +y -x
+ 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(origin, (1, 0, 0), (0, 0, 1)),
+ 'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)),
+ 'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)),
+ 'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)),
+ 'YX': Plane(origin, (0, 1, 0), (0, 0, -1)),
+ 'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)),
+ 'front': Plane(origin, (1, 0, 0), (0, 0, 1)),
+ 'back': Plane(origin, (-1, 0, 0), (0, 0, -1)),
+ 'left': Plane(origin, (0, 0, 1), (-1, 0, 0)),
+ 'right': Plane(origin, (0, 0, -1), (1, 0, 0)),
+ 'top': Plane(origin, (1, 0, 0), (0, 1, 0)),
+ 'bottom': Plane(origin, (1, 0, 0), (0, -1, 0))
+ }
+
+ try:
+ return namedPlanes[stdName]
+ except KeyError:
+ raise ValueError('Supported names are {}'.format(
+ namedPlanes.keys()))
+
+ @classmethod
+ def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('XY', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
+ plane = Plane.named('YZ', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
+ plane = Plane.named('ZX', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('XZ', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)):
+ plane = Plane.named('YX', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
+ plane = Plane.named('ZY', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('front', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)):
+ plane = Plane.named('back', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)):
+ plane = Plane.named('left', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)):
+ plane = Plane.named('right', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('top', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ @classmethod
+ def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
+ plane = Plane.named('bottom', origin)
+ plane._setPlaneDir(xDir)
+ return plane
+
+ 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.
+ """
+ normal = Vector(normal)
+ if (normal.Length == 0.0):
+ raise ValueError('normal should be non null')
+ self.zDir = normal.normalized()
+ xDir = Vector(xDir)
+ if (xDir.Length == 0.0):
+ raise ValueError('xDir should be non null')
+ self._setPlaneDir(xDir)
+
+ self.invZDir = self.zDir.multiply(-1.0)
+
+ self.origin = origin
+
+ @property
+ def origin(self):
+ return self._origin
+
+ @origin.setter
+ def origin(self, value):
+ self._origin = Vector(value)
+ self._calcTransforms()
+
+ def setOrigin2d(self, x, y):
+ """
+ Set a new origin in the plane itself
+
+ Set a new origin in the plane itself. 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. Both operations were relative to local coordinates of the
+ plane.
+ """
+ self.origin = self.toWorldCoords((x, y))
+
+ def isWireInside(self, baseWire, testWire):
+ """Determine if testWire is inside baseWire
+
+ 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
+ return bb == BoundBox.findOutsideBox2D(bb, tb)
+
+ 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, 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, cadquery.Shape):
+ return obj.transformShape(self.rG)
+ else:
+ raise ValueError(
+ "Don't know how to convert type {} to local coordinates".format(
+ 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 isinstance(tuplePoint, Vector):
+ v = tuplePoint
+ elif len(tuplePoint) == 2:
+ v = Vector(tuplePoint[0], tuplePoint[1], 0)
+ else:
+ v = Vector(tuplePoint)
+ 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
+
+ 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.
+ """
+ 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))
+
+ return Plane(self.origin, newXdir, newZdir)
+
+ def rotateShapes(self, listOfShapes, rotationMatrix):
+ """Rotate the listOfShapes by the supplied rotationMatrix
+
+ @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 coordinates.
+
+ resultWires = []
+ for w in listOfShapes:
+ mirrored = w.transformGeometry(rotationMatrix.wrapped)
+
+ # If the first vertex of the second wire is not coincident with the
+ # first or last vertices of the first wire we have to fix the wire
+ # so that it will mirror correctly.
+ if ((mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[0].X and
+ mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[0].Y and
+ mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[0].Z) or
+ (mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[-1].X and
+ mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[-1].Y and
+ mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[-1].Z)):
+
+ resultWires.append(mirrored)
+ else:
+ # Make sure that our mirrored edges meet up and are ordered
+ # properly.
+ aEdges = w.wrapped.Edges
+ aEdges.extend(mirrored.wrapped.Edges)
+ comp = FreeCADPart.Compound(aEdges)
+ mirroredWire = comp.connectEdgesToWires(False).Wires[0]
+
+ resultWires.append(cadquery.Shape.cast(mirroredWire))
+
+ return resultWires
+
+ def _setPlaneDir(self, xDir):
+ """Set the vectors parallel to the plane, i.e. xDir and yDir"""
+ if (self.zDir.dot(xDir) > 1e-5):
+ raise ValueError('xDir must be parralel to the plane')
+ xDir = Vector(xDir)
+ self.xDir = xDir.normalized()
+ self.yDir = self.zDir.cross(self.xDir).normalized()
+
+ def _calcTransforms(self):
+ """Computes transformation matrices to convert between coordinates
+
+ Computes transformation matrices to convert between 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 translation and the rotation.
+ # the double-inverting is strange, and I don't 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 = self.origin.x
+ invR.A24 = self.origin.y
+ invR.A34 = self.origin.z
+
+ self.rG = invR
+ self.fG = invR.inverse()
+
+ def computeTransform(self, tMatrix):
+ """Computes the 2-d projection of the supplied matrix"""
+
+ return Matrix(self.fG.multiply(tMatrix.wrapped).multiply(self.rG))
+
+
+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 isinstance(obj, tuple):
+ tmp.add(obj[0], obj[1], obj[2])
+ elif isinstance(obj, Vector):
+ tmp.add(obj.fV)
+ elif isinstance(obj, BoundBox):
+ tmp.add(obj.wrapped)
+
+ return BoundBox(tmp)
+
+ @classmethod
+ def findOutsideBox2D(cls, b1, b2):
+ """Compares bounding boxes
+
+ 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
+ doesn't work correctly plus, there was all kinds of rounding error in
+ the built-in implementation i do not understand.
+ """
+ fc_bb1 = b1.wrapped
+ fc_bb2 = b2.wrapped
+ if (fc_bb1.XMin < fc_bb2.XMin and
+ fc_bb1.XMax > fc_bb2.XMax and
+ fc_bb1.YMin < fc_bb2.YMin and
+ fc_bb1.YMax > fc_bb2.YMax):
+ return b1
+
+ if (fc_bb2.XMin < fc_bb1.XMin and
+ fc_bb2.XMax > fc_bb1.XMax and
+ fc_bb2.YMin < fc_bb1.YMin and
+ fc_bb2.YMax > fc_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/build/lib.linux-i686-2.7/cadquery/freecad_impl/importers.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/importers.py
new file mode 100644
index 0000000..7d4f0a9
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/importers.py
@@ -0,0 +1,71 @@
+
+import cadquery
+from .shapes import Shape
+
+import FreeCAD
+import Part
+import sys
+import os
+import urllib as urlreader
+import tempfile
+
+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.Workplane 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:
+ #print fileName
+ rshape = Part.read(fileName)
+
+ #Make sure that we extract all the solids
+ solids = []
+ for solid in rshape.Solids:
+ solids.append(Shape.cast(solid))
+
+ return cadquery.Workplane("XY").newObject(solids)
+ except:
+ raise ValueError("STEP File Could not be loaded")
+
+#Loads a STEP file from an URL into a CQ.Workplane object
+def importStepFromURL(url):
+ #Now read and return the shape
+ try:
+ webFile = urlreader.urlopen(url)
+ tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False)
+ tempFile.write(webFile.read())
+ webFile.close()
+ tempFile.close()
+
+ rshape = Part.read(tempFile.name)
+
+ #Make sure that we extract all the solids
+ solids = []
+ for solid in rshape.Solids:
+ solids.append(Shape.cast(solid))
+
+ return cadquery.Workplane("XY").newObject(solids)
+ except:
+ raise ValueError("STEP File from the URL: " + url + " Could not be loaded")
diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/shapes.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/shapes.py
new file mode 100644
index 0000000..76af1c1
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/shapes.py
@@ -0,0 +1,982 @@
+"""
+ Copyright (C) 2011-2015 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 implementations
+
+ 2. Allow better documentation. One of the reasons FreeCAD is no more popular is because
+ its docs are terrible. This allows us to provide 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 'forConstruction' 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 user friendly: we like to change those when necessary. 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
+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
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @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):
+ # A Part.Shape object doesn't have the CenterOfMass function, but it's wrapped Solid(s) does
+ if isinstance(self.wrapped, FreeCADPart.Shape):
+ # If there are no Solids, we're probably dealing with a Face or something similar
+ if len(self.Solids()) == 0:
+ return Vector(self.wrapped.CenterOfMass)
+ elif len(self.Solids()) == 1:
+ return Vector(self.Solids()[0].wrapped.CenterOfMass)
+ elif len(self.Solids()) > 1:
+ return self.CombinedCenter(self.Solids())
+ elif isinstance(self.wrapped, FreeCADPart.Solid):
+ return Vector(self.wrapped.CenterOfMass)
+ else:
+ raise ValueError("Cannot find the center of %s object type" % str(type(self.Solids()[0].wrapped)))
+
+ def CenterOfBoundBox(self):
+ if isinstance(self.wrapped, FreeCADPart.Shape):
+ # If there are no Solids, we're probably dealing with a Face or something similar
+ if len(self.Solids()) == 0:
+ return Vector(self.wrapped.BoundBox.Center)
+ elif len(self.Solids()) == 1:
+ return Vector(self.Solids()[0].wrapped.BoundBox.Center)
+ elif len(self.Solids()) > 1:
+ return self.CombinedCenterOfBoundBox(self.Solids())
+ elif isinstance(self.wrapped, FreeCADPart.Solid):
+ return Vector(self.wrapped.BoundBox.Center)
+ else:
+ raise ValueError("Cannot find the center(BoundBox's) of %s object type" % str(type(self.Solids()[0].wrapped)))
+
+ @staticmethod
+ def CombinedCenter(objects):
+ """
+ Calculates the center of mass of multiple objects.
+
+ :param objects: a list of objects with mass
+ """
+ total_mass = sum(o.wrapped.Mass for o in objects)
+ weighted_centers = [o.wrapped.CenterOfMass.multiply(o.wrapped.Mass) for o in objects]
+
+ sum_wc = weighted_centers[0]
+ for wc in weighted_centers[1:] :
+ sum_wc = sum_wc.add(wc)
+
+ return Vector(sum_wc.multiply(1./total_mass))
+
+ @staticmethod
+ def CombinedCenterOfBoundBox(objects):
+ """
+ Calculates the center of BoundBox of multiple objects.
+
+ :param objects: a list of objects with mass 1
+ """
+ total_mass = len(objects)
+ weighted_centers = [o.wrapped.BoundBox.Center.multiply(1.0) for o in objects]
+
+ sum_wc = weighted_centers[0]
+ for wc in weighted_centers[1:] :
+ sum_wc = sum_wc.add(wc)
+
+ return Vector(sum_wc.multiply(1./total_mass))
+
+ 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):
+ """
+ A Single Point in Space
+ """
+
+ 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
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ 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):
+ """
+ A trimmed curve that represents the border of a face
+ """
+
+ 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'
+ }
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ 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):
+ """
+ A series of connected, ordered Edges, that typically bounds a Face
+ """
+
+ def __init__(self, obj):
+ """
+ A Wire
+ """
+ self.wrapped = obj
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @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))
+
+ def clean(self):
+ """This method is not implemented yet."""
+ return self
+
+class Face(Shape):
+ """
+ a bounded surface that represents part of the boundary of a solid
+ """
+ def __init__(self, obj):
+
+ self.wrapped = obj
+
+ self.facetypes = {
+ # TODO: bezier,bspline etc
+ FreeCADPart.Plane: 'PLANE',
+ FreeCADPart.Sphere: 'SPHERE',
+ FreeCADPart.Cone: 'CONE'
+ }
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ 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):
+ """
+ the outer boundary of a surface
+ """
+ def __init__(self, wrapped):
+ """
+ A Shell
+ """
+ self.wrapped = wrapped
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @classmethod
+ def makeShell(cls, listOfFaces):
+ return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces]))
+
+
+class Solid(Shape):
+ """
+ a single solid
+ """
+ def __init__(self, obj):
+ """
+ A Solid
+ """
+ self.wrapped = obj
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ @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 in pnt with the dimensions (length,width,height)
+ By 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):
+ """
+ Make a cone with given radii and height
+ By 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, ruled=False):
+ """
+ makes a loft from a list of wires
+ The wires will be converted into faces when possible-- it is presumed that nobody ever actually
+ 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, ruled))
+
+ @classmethod
+ def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None):
+ """
+ Make a wedge located in pnt
+ By default pnt=Vector(0,0,0) and dir=Vector(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, dir=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None):
+ """
+ Make a sphere with a given radius
+ By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360
+ """
+ return Shape.cast(FreeCADPart.makeSphere(radius, pnt.wrapped, dir.wrapped, 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 are:
+ (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
+ (4) 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 clean(self):
+ """Clean faces by removing splitter edges."""
+ r = self.wrapped.removeSplitter()
+ # removeSplitter() returns a generic Shape type, cast to actual type of object
+ r = FreeCADPart.cast_to_shape(r)
+ return Shape.cast(r)
+
+ 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 chamfer(self, length, length2, edgeList):
+ """
+ Chamfers the specified edges of this solid.
+ :param length: length > 0, the length (length) of the chamfer
+ :param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required.
+ :param edgeList: a list of Edge objects, which must belong to this solid
+ :return: Chamfered solid
+ """
+ nativeEdges = [e.wrapped for e in edgeList]
+ # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API
+ if length2:
+ return Shape.cast(self.wrapped.makeChamfer(length, length2, nativeEdges))
+ else:
+ return Shape.cast(self.wrapped.makeChamfer(length, 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):
+ """
+ a collection of disconnected solids
+ """
+
+ def __init__(self, obj):
+ """
+ An Edge
+ """
+ self.wrapped = obj
+
+ # Helps identify this solid through the use of an ID
+ self.label = ""
+
+ def Center(self):
+ return self.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)
+
+ def clean(self):
+ """This method is not implemented yet."""
+ return self
diff --git a/build/lib.linux-i686-2.7/cadquery/plugins/__init__.py b/build/lib.linux-i686-2.7/cadquery/plugins/__init__.py
new file mode 100644
index 0000000..3697b9f
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/plugins/__init__.py
@@ -0,0 +1,18 @@
+"""
+ CadQuery
+ Copyright (C) 2015 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/build/lib.linux-i686-2.7/cadquery/selectors.py b/build/lib.linux-i686-2.7/cadquery/selectors.py
new file mode 100644
index 0000000..be07d7b
--- /dev/null
+++ b/build/lib.linux-i686-2.7/cadquery/selectors.py
@@ -0,0 +1,474 @@
+"""
+ Copyright (C) 2011-2015 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
+
+ def __and__(self, other):
+ return AndSelector(self, other)
+
+ def __add__(self, other):
+ return SumSelector(self, other)
+
+ def __sub__(self, other):
+ return SubtractSelector(self, other)
+
+ def __neg__(self):
+ return InverseSelector(self)
+
+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(Vector(*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 BoxSelector(Selector):
+ """
+ Selects objects inside the 3D box defined by 2 points.
+
+ If `boundingbox` is True only the objects that have their bounding
+ box inside the given box is selected. Otherwise only center point
+ of the object is tested.
+
+ Applicability: all types of shapes
+
+ Example::
+
+ CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1))
+ """
+ def __init__(self, point0, point1, boundingbox=False):
+ self.p0 = Vector(*point0)
+ self.p1 = Vector(*point1)
+ self.test_boundingbox = boundingbox
+
+ def filter(self, objectList):
+
+ result = []
+ x0, y0, z0 = self.p0.toTuple()
+ x1, y1, z1 = self.p1.toTuple()
+
+ def isInsideBox(p):
+ # using XOR for checking if x/y/z is in between regardless
+ # of order of x/y/z0 and x/y/z1
+ return ((p.x < x0) ^ (p.x < x1)) and \
+ ((p.y < y0) ^ (p.y < y1)) and \
+ ((p.z < z0) ^ (p.z < z1))
+
+ for o in objectList:
+ if self.test_boundingbox:
+ bb = o.BoundingBox()
+ if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \
+ isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)):
+ result.append(o)
+ else:
+ if isInsideBox(o.Center()):
+ result.append(o)
+
+ return result
+
+class BaseDirSelector(Selector):
+ """
+ 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, tolerance=0.0001):
+ self.vector = vector
+ self.max = max
+ self.directionMax = directionMax
+ self.TOLERANCE = tolerance
+ def filter(self,objectList):
+
+ 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)
+
+ # find out the max/min distance
+ if self.directionMax:
+ d = max(map(distance, objectList))
+ else:
+ d = min(map(distance, objectList))
+
+ # return all objects at the max/min distance (within a tolerance)
+ return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList)
+
+class BinarySelector(Selector):
+ """
+ Base class for selectors that operates with two other
+ selectors. Subclass must implement the :filterResults(): method.
+ """
+ def __init__(self, left, right):
+ self.left = left
+ self.right = right
+
+ def filter(self, objectList):
+ return self.filterResults(self.left.filter(objectList),
+ self.right.filter(objectList))
+
+ def filterResults(self, r_left, r_right):
+ raise NotImplementedError
+
+class AndSelector(BinarySelector):
+ """
+ Intersection selector. Returns objects that is selected by both selectors.
+ """
+ def filterResults(self, r_left, r_right):
+ # return intersection of lists
+ return list(set(r_left) & set(r_right))
+
+class SumSelector(BinarySelector):
+ """
+ Union selector. Returns the sum of two selectors results.
+ """
+ def filterResults(self, r_left, r_right):
+ # return the union (no duplicates) of lists
+ return list(set(r_left + r_right))
+
+class SubtractSelector(BinarySelector):
+ """
+ Difference selector. Substract results of a selector from another
+ selectors results.
+ """
+ def filterResults(self, r_left, r_right):
+ return list(set(r_left) - set(r_right))
+
+class InverseSelector(Selector):
+ """
+ Inverts the selection of given selector. In other words, selects
+ all objects that is not selected by given selector.
+ """
+ def __init__(self, selector):
+ self.selector = selector
+
+ def filter(self, objectList):
+ # note that Selector() selects everything
+ return SubtractSelector(Selector(), self.selector).filter(objectList)
+
+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/build/lib.linux-i686-2.7/tests/TestCQGI.py b/build/lib.linux-i686-2.7/tests/TestCQGI.py
new file mode 100644
index 0000000..35d8906
--- /dev/null
+++ b/build/lib.linux-i686-2.7/tests/TestCQGI.py
@@ -0,0 +1,170 @@
+"""
+ Tests CQGI functionality
+
+ Currently, this includes:
+ Parsing a script, and detecting its available variables
+ Altering the values at runtime
+ defining a build_object function to return results
+"""
+
+from cadquery import cqgi
+from tests import BaseTest
+import textwrap
+
+TESTSCRIPT = textwrap.dedent(
+ """
+ height=2.0
+ width=3.0
+ (a,b) = (1.0,1.0)
+ foo="bar"
+
+ result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) )
+ build_object(result)
+ """
+)
+
+
+class TestCQGI(BaseTest):
+ def test_parser(self):
+ model = cqgi.CQModel(TESTSCRIPT)
+ metadata = model.metadata
+
+ self.assertEquals(set(metadata.parameters.keys()), {'height', 'width', 'a', 'b', 'foo'})
+
+ def test_build_with_empty_params(self):
+ model = cqgi.CQModel(TESTSCRIPT)
+ result = model.build()
+
+ self.assertTrue(result.success)
+ self.assertTrue(len(result.results) == 1)
+ self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0")
+
+ def test_build_with_different_params(self):
+ model = cqgi.CQModel(TESTSCRIPT)
+ result = model.build({'height': 3.0})
+ self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0")
+
+ def test_build_with_exception(self):
+ badscript = textwrap.dedent(
+ """
+ raise ValueError("ERROR")
+ """
+ )
+
+ model = cqgi.CQModel(badscript)
+ result = model.build({})
+ self.assertFalse(result.success)
+ self.assertIsNotNone(result.exception)
+ self.assertTrue(result.exception.message == "ERROR")
+
+ def test_that_invalid_syntax_in_script_fails_immediately(self):
+ badscript = textwrap.dedent(
+ """
+ this doesnt even compile
+ """
+ )
+
+ with self.assertRaises(Exception) as context:
+ model = cqgi.CQModel(badscript)
+
+ self.assertTrue('invalid syntax' in context.exception)
+
+ def test_that_two_results_are_returned(self):
+ script = textwrap.dedent(
+ """
+ h = 1
+ build_object(h)
+ h = 2
+ build_object(h)
+ """
+ )
+
+ model = cqgi.CQModel(script)
+ result = model.build({})
+ self.assertEquals(2, len(result.results))
+ self.assertEquals(1, result.results[0])
+ self.assertEquals(2, result.results[1])
+
+ def test_that_assinging_number_to_string_works(self):
+ script = textwrap.dedent(
+ """
+ h = "this is a string"
+ build_object(h)
+ """
+ )
+ result = cqgi.parse(script).build( {'h': 33.33})
+ self.assertEquals(result.results[0], "33.33")
+
+ def test_that_assigning_string_to_number_fails(self):
+ script = textwrap.dedent(
+ """
+ h = 20.0
+ build_object(h)
+ """
+ )
+ result = cqgi.parse(script).build( {'h': "a string"})
+ self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
+
+ def test_that_assigning_unknown_var_fails(self):
+ script = textwrap.dedent(
+ """
+ h = 20.0
+ build_object(h)
+ """
+ )
+
+ result = cqgi.parse(script).build( {'w': "var is not there"})
+ self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
+
+ def test_that_not_calling_build_object_raises_error(self):
+ script = textwrap.dedent(
+ """
+ h = 20.0
+ """
+ )
+ result = cqgi.parse(script).build()
+ self.assertTrue(isinstance(result.exception, cqgi.NoOutputError))
+
+ def test_that_cq_objects_are_visible(self):
+ script = textwrap.dedent(
+ """
+ r = cadquery.Workplane('XY').box(1,2,3)
+ build_object(r)
+ """
+ )
+
+ result = cqgi.parse(script).build()
+ self.assertTrue(result.success)
+ self.assertIsNotNone(result.first_result)
+
+ def test_setting_boolean_variable(self):
+ script = textwrap.dedent(
+ """
+ h = True
+ build_object( "*%s*" % str(h) )
+ """
+ )
+
+ #result = cqgi.execute(script)
+ result = cqgi.parse(script).build({'h': False})
+
+ self.assertTrue(result.success)
+ self.assertEquals(result.first_result,'*False*')
+
+ def test_that_only_top_level_vars_are_detected(self):
+ script = textwrap.dedent(
+ """
+ h = 1.0
+ w = 2.0
+
+ def do_stuff():
+ x = 1
+ y = 2
+
+ build_object( "result" )
+ """
+ )
+
+ model = cqgi.parse(script)
+
+ self.assertEquals(2, len(model.metadata.parameters))
\ No newline at end of file
diff --git a/build/lib.linux-i686-2.7/tests/TestCQSelectors.py b/build/lib.linux-i686-2.7/tests/TestCQSelectors.py
new file mode 100644
index 0000000..f90e14b
--- /dev/null
+++ b/build/lib.linux-i686-2.7/tests/TestCQSelectors.py
@@ -0,0 +1,358 @@
+__author__ = 'dcowden'
+
+"""
+ Tests for CadQuery Selectors
+
+ These tests do not construct any solids, they test only selectors that query
+ an existing solid
+
+"""
+
+import math
+import unittest,sys
+import os.path
+
+#my modules
+from tests import BaseTest,makeUnitCube,makeUnitSquareWire
+from cadquery import *
+from cadquery import selectors
+
+class TestCQSelectors(BaseTest):
+
+
+ def testWorkplaneCenter(self):
+ "Test Moving workplane center"
+ s = Workplane(Plane.XY())
+
+ #current point and world point should be equal
+ self.assertTupleAlmostEquals((0.0,0.0,0.0),s.plane.origin.toTuple(),3)
+
+ #move origin and confirm center moves
+ s.center(-2.0,-2.0)
+
+ #current point should be 0,0, but
+
+ self.assertTupleAlmostEquals((-2.0,-2.0,0.0),s.plane.origin.toTuple(),3)
+
+
+ def testVertices(self):
+ t = makeUnitSquareWire() # square box
+ c = CQ(t)
+
+ self.assertEqual(4,c.vertices().size() )
+ self.assertEqual(4,c.edges().size() )
+ self.assertEqual(0,c.vertices().edges().size() ) #no edges on any vertices
+ self.assertEqual(4,c.edges().vertices().size() ) #but selecting all edges still yields all vertices
+ self.assertEqual(1,c.wires().size()) #just one wire
+ self.assertEqual(0,c.faces().size())
+ self.assertEqual(0,c.vertices().faces().size()) #odd combinations all work but yield no results
+ self.assertEqual(0,c.edges().faces().size())
+ self.assertEqual(0,c.edges().vertices().faces().size())
+
+ def testEnd(self):
+ c = CQ(makeUnitSquareWire())
+ self.assertEqual(4,c.vertices().size() ) #4 because there are 4 vertices
+ self.assertEqual(1,c.vertices().end().size() ) #1 because we started with 1 wire
+
+ def testAll(self):
+ "all returns a list of CQ objects, so that you can iterate over them individually"
+ c = CQ(makeUnitCube())
+ self.assertEqual(6,c.faces().size())
+ self.assertEqual(6,len(c.faces().all()))
+ self.assertEqual(4,c.faces().all()[0].vertices().size() )
+
+ def testFirst(self):
+ c = CQ( makeUnitCube())
+ self.assertEqual(type(c.vertices().first().val()),Vertex)
+ self.assertEqual(type(c.vertices().first().first().first().val()),Vertex)
+
+ def testCompounds(self):
+ c = CQ(makeUnitSquareWire())
+ self.assertEqual(0,c.compounds().size() )
+ self.assertEqual(0,c.shells().size() )
+ self.assertEqual(0,c.solids().size() )
+
+ def testSolid(self):
+ c = CQ(makeUnitCube())
+ #make sure all the counts are right for a cube
+ self.assertEqual(1,c.solids().size() )
+ self.assertEqual(6,c.faces().size() )
+ self.assertEqual(12,c.edges().size())
+ self.assertEqual(8,c.vertices().size() )
+ self.assertEqual(0,c.compounds().size())
+
+ #now any particular face should result in 4 edges and four vertices
+ self.assertEqual(4,c.faces().first().edges().size() )
+ self.assertEqual(1,c.faces().first().size() )
+ self.assertEqual(4,c.faces().first().vertices().size() )
+
+ self.assertEqual(4,c.faces().last().edges().size() )
+
+
+
+ def testFaceTypesFilter(self):
+ "Filters by face type"
+ c = CQ(makeUnitCube())
+ self.assertEqual(c.faces().size(), c.faces('%PLANE').size())
+ self.assertEqual(c.faces().size(), c.faces('%plane').size())
+ self.assertEqual(0, c.faces('%sphere').size())
+ self.assertEqual(0, c.faces('%cone').size())
+ self.assertEqual(0, c.faces('%SPHERE').size())
+
+ def testPerpendicularDirFilter(self):
+ c = CQ(makeUnitCube())
+
+ self.assertEqual(8,c.edges("#Z").size() ) #8 edges are perp. to z
+ self.assertEqual(4, c.faces("#Z").size()) #4 faces are perp to z too!
+
+ def testFaceDirFilter(self):
+ c = CQ(makeUnitCube())
+ #a cube has one face in each direction
+ self.assertEqual(1, c.faces("+Z").size())
+ self.assertEqual(1, c.faces("-Z").size())
+ self.assertEqual(1, c.faces("+X").size())
+ self.assertEqual(1, c.faces("X").size()) #should be same as +X
+ self.assertEqual(1, c.faces("-X").size())
+ self.assertEqual(1, c.faces("+Y").size())
+ self.assertEqual(1, c.faces("-Y").size())
+ self.assertEqual(0, c.faces("XY").size())
+
+ def testParallelPlaneFaceFilter(self):
+ c = CQ(makeUnitCube())
+
+ #faces parallel to Z axis
+ self.assertEqual(2, c.faces("|Z").size())
+ #TODO: provide short names for ParallelDirSelector
+ self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,1)))).size()) #same thing as above
+ self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,-1)))).size()) #same thing as above
+
+ #just for fun, vertices on faces parallel to z
+ self.assertEqual(8, c.faces("|Z").vertices().size())
+
+ def testParallelEdgeFilter(self):
+ c = CQ(makeUnitCube())
+ self.assertEqual(4, c.edges("|Z").size())
+ self.assertEqual(4, c.edges("|X").size())
+ self.assertEqual(4, c.edges("|Y").size())
+
+ def testMaxDistance(self):
+ c = CQ(makeUnitCube())
+
+ #should select the topmost face
+ self.assertEqual(1, c.faces(">Z").size())
+ self.assertEqual(4, c.faces(">Z").vertices().size())
+
+ #vertices should all be at z=1, if this is the top face
+ self.assertEqual(4, len(c.faces(">Z").vertices().vals() ))
+ for v in c.faces(">Z").vertices().vals():
+ self.assertAlmostEqual(1.0,v.Z,3)
+
+ # test the case of multiple objects at the same distance
+ el = c.edges(" 0,0,1
+ e = c.edges(selectors.NearestToPointSelector(t)).vals()[0]
+ v = c.edges(selectors.NearestToPointSelector(t)).vertices().vals()
+ self.assertEqual(2,len(v))
+
+ #nearest solid is myself
+ s = c.solids(selectors.NearestToPointSelector(t)).vals()
+ self.assertEqual(1,len(s))
+
+ def testBox(self):
+ c = CQ(makeUnitCube())
+
+ # test vertice selection
+ test_data_vertices = [
+ # box point0, box point1, selected vertice
+ ((0.9, 0.9, 0.9), (1.1, 1.1, 1.1), (1.0, 1.0, 1.0)),
+ ((-0.1, 0.9, 0.9), (0.9, 1.1, 1.1), (0.0, 1.0, 1.0)),
+ ((-0.1, -0.1, 0.9), (0.1, 0.1, 1.1), (0.0, 0.0, 1.0)),
+ ((-0.1, -0.1, -0.1), (0.1, 0.1, 0.1), (0.0, 0.0, 0.0)),
+ ((0.9, -0.1, -0.1), (1.1, 0.1, 0.1), (1.0, 0.0, 0.0)),
+ ((0.9, 0.9, -0.1), (1.1, 1.1, 0.1), (1.0, 1.0, 0.0)),
+ ((-0.1, 0.9, -0.1), (0.1, 1.1, 0.1), (0.0, 1.0, 0.0)),
+ ((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0))
+ ]
+
+ for d in test_data_vertices:
+ vl = c.vertices(selectors.BoxSelector(d[0], d[1])).vals()
+ self.assertEqual(1, len(vl))
+ v = vl[0]
+ self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
+
+ # this time box points are swapped
+ vl = c.vertices(selectors.BoxSelector(d[1], d[0])).vals()
+ self.assertEqual(1, len(vl))
+ v = vl[0]
+ self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
+
+ # test multiple vertices selection
+ vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, 0.9),(0.1, 1.1, 1.1))).vals()
+ self.assertEqual(2, len(vl))
+ vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, -0.1),(0.1, 1.1, 1.1))).vals()
+ self.assertEqual(4, len(vl))
+
+ # test edge selection
+ test_data_edges = [
+ # box point0, box point1, edge center
+ ((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)),
+ ((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)),
+ ((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)),
+ ((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0))
+ ]
+
+ for d in test_data_edges:
+ el = c.edges(selectors.BoxSelector(d[0], d[1])).vals()
+ self.assertEqual(1, len(el))
+ ec = el[0].Center()
+ self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
+
+ # test again by swapping box points
+ el = c.edges(selectors.BoxSelector(d[1], d[0])).vals()
+ self.assertEqual(1, len(el))
+ ec = el[0].Center()
+ self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
+
+ # test multiple edge selection
+ el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals()
+ self.assertEqual(2, len(el))
+ el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals()
+ self.assertEqual(3, len(el))
+
+ # test face selection
+ test_data_faces = [
+ # box point0, box point1, face center
+ ((0.4, -0.1, 0.4), (0.6, 0.1, 0.6), (0.5, 0.0, 0.5)),
+ ((0.9, 0.4, 0.4), (1.1, 0.6, 0.6), (1.0, 0.5, 0.5)),
+ ((0.4, 0.4, 0.9), (0.6, 0.6, 1.1), (0.5, 0.5, 1.0)),
+ ((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0))
+ ]
+
+ for d in test_data_faces:
+ fl = c.faces(selectors.BoxSelector(d[0], d[1])).vals()
+ self.assertEqual(1, len(fl))
+ fc = fl[0].Center()
+ self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
+
+ # test again by swapping box points
+ fl = c.faces(selectors.BoxSelector(d[1], d[0])).vals()
+ self.assertEqual(1, len(fl))
+ fc = fl[0].Center()
+ self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
+
+ # test multiple face selection
+ fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals()
+ self.assertEqual(2, len(fl))
+ fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals()
+ self.assertEqual(3, len(fl))
+
+ # test boundingbox option
+ el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals()
+ self.assertEqual(1, len(el))
+ fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals()
+ self.assertEqual(0, len(fl))
+ fl = c.faces(selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals()
+ self.assertEqual(1, len(fl))
+
+ def testAndSelector(self):
+ c = CQ(makeUnitCube())
+
+ S = selectors.StringSyntaxSelector
+ BS = selectors.BoxSelector
+
+ el = c.edges(selectors.AndSelector(S('|X'), BS((-2,-2,0.1), (2,2,2)))).vals()
+ self.assertEqual(2, len(el))
+
+ # test 'and' (intersection) operator
+ el = c.edges(S('|X') & BS((-2,-2,0.1), (2,2,2))).vals()
+ self.assertEqual(2, len(el))
+
+ def testSumSelector(self):
+ c = CQ(makeUnitCube())
+
+ S = selectors.StringSyntaxSelector
+
+ fl = c.faces(selectors.SumSelector(S(">Z"), S("Z") + S("X"))).vals()
+ self.assertEqual(3, len(fl))
+
+ # test the subtract operator
+ fl = c.faces(S("#Z") - S(">X")).vals()
+ self.assertEqual(3, len(fl))
+
+ def testInverseSelector(self):
+ c = CQ(makeUnitCube())
+
+ S = selectors.StringSyntaxSelector
+
+ fl = c.faces(selectors.InverseSelector(S('>Z'))).vals()
+ self.assertEqual(5, len(fl))
+ el = c.faces('>Z').edges(selectors.InverseSelector(S('>X'))).vals()
+ self.assertEqual(3, len(el))
+
+ # test invert operator
+ fl = c.faces(-S('>Z')).vals()
+ self.assertEqual(5, len(fl))
+ el = c.faces('>Z').edges(-S('>X')).vals()
+ self.assertEqual(3, len(el))
+
+ def testFaceCount(self):
+ c = CQ(makeUnitCube())
+ self.assertEqual( 6, c.faces().size() )
+ self.assertEqual( 2, c.faces("|Z").size() )
+
+ def testVertexFilter(self):
+ "test selecting vertices on a face"
+ c = CQ(makeUnitCube())
+
+ #TODO: filters work ok, but they are in global coordinates which sux. it would be nice
+ #if they were available in coordinates local to the selected face
+
+ v2 = c.faces("+Z").vertices("
+
+
+
+
+
+
+