diff --git a/cadquery/CQ.py b/cadquery/CQ.py
index e356018..d63ba04 100644
--- a/cadquery/CQ.py
+++ b/cadquery/CQ.py
@@ -17,6 +17,10 @@
License along with this library; If not, see
"""
+import time,math
+from cadquery import *
+from cadquery import selectors
+
class CQContext(object):
"""
A shared context for modeling.
@@ -431,7 +435,7 @@ class CQ(object):
if selector is not None:
if type(selector) == str:
- selectorObj = StringSyntaxSelector(selector)
+ selectorObj = selectors.StringSyntaxSelector(selector)
else:
selectorObj = selector
toReturn = selectorObj.filter(toReturn)
@@ -631,7 +635,7 @@ class CQ(object):
:type fileName: String, absolute path to the file
"""
- SVGexporter.exportSVG(self.val().wrapped,fileName)
+ exporters.exportSVG(self,fileName)
def rotateAboutCenter(self,axisEndPoint,angleDegrees):
"""
@@ -762,3 +766,1412 @@ class CQ(object):
solid.wrapped = s.wrapped
return self.newObject([s])
+class Workplane(CQ):
+ """
+ Defines a coordinate system in space, in which 2-d coordinates can be used.
+
+ :param plane: the plane in which the workplane will be done
+ :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
+ :raises: ValueError if the provided plane is not a plane, a valid named workplane
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane, and the
+ *current point* is on the origin.
+
+ .. note::
+ You can also create workplanes on the surface of existing faces using
+ :py:meth:`CQ.workplane`
+
+
+ """
+
+ FOR_CONSTRUCTION = 'ForConstruction'
+
+
+ def __init__(self, inPlane ,origin=(0,0,0), obj=None):
+ """
+ make a workplane from a particular plane
+
+ :param plane: the plane in which the workplane will be done
+ :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
+ :param origin: the desired origin of the new workplane
+ :type origin: a 3-tuple in global coordinates, or None to default to the origin
+ :param obj: an object to use initially for the stack
+ :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
+ :raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ
+ :return: A Workplane object, with coordinate system matching the supplied plane.
+
+ The most common use is::
+
+ s = Workplane("XY")
+
+ After creation, the stack contains a single point, the origin of the underlying plane, and the
+ *current point* is on the origin.
+ """
+
+ if inPlane.__class__.__name__ == 'Plane':
+ tmpPlane = inPlane
+ elif type(inPlane) == str:
+ tmpPlane = Plane.named(inPlane,origin)
+ else:
+ tmpPlane = None
+
+ if tmpPlane == None:
+ raise ValueError(" Provided value %s is not a valid work plane." % str(inPlane))
+
+ self.obj = obj
+ self.plane = tmpPlane
+ self.firstPoint = None
+ self.objects = [self.plane.origin] #changed so that workplane has the center as the first item on the stack
+ self.parent = None
+ self.ctx = CQContext()
+
+ def transformed(self,rotate=Vector(0,0,0),offset=Vector(0,0,0)):
+ """
+ Create a new workplane based on the current one.
+ The origin of the new plane is located at the existing origin+offset vector, where offset is given in
+ coordinates local to the current plane
+ The new plane is rotated through the angles specified by the components of the rotation vector
+ :param rotate: vector of angles to rotate, in degrees relative to work plane coordinates
+ :param offset: vector to offset the new plane, in local work plane coordinates
+ :return: a new work plane, transformed as requested
+ """
+ p = self.plane.rotated(rotate)
+ p.setOrigin3d(self.plane.toWorldCoords(offset.toTuple() ))
+ ns = self.newObject([p.origin])
+ ns.plane = p
+
+ return ns
+
+ def newObject(self,objlist):
+ """
+ Create a new workplane object from this one.
+
+ Overrides CQ.newObject, and should be used by extensions, plugins, and
+ subclasses to create new objects.
+
+ :param objlist: new objects to put on the stack
+ :type objlist: a list of CAD primitives
+ :return: a new Workplane object with the current workplane as a parent.
+
+ """
+
+ #copy the current state to the new object
+ ns = Workplane("XY")
+ ns.plane = self.plane
+ ns.parent = self
+ ns.objects = list(objlist)
+ ns.ctx = self.ctx
+ return ns
+
+ def _findFromPoint(self,useLocalCoords=False):
+ """
+ finds the start point for an operation when an existing point
+ is implied. Examples include 2d operations such as lineTo,
+ which allows specifying the end point, and implicitly use the
+ end of the previous line as the starting point
+
+ :return: a Vector representing the point to use, or none if
+ such a point is not available.
+
+ :param useLocalCoords: selects whether the point is returned
+ in local coordinates or global coordinates.
+
+ The algorithm is this:
+ * If an Edge is on the stack, its end point is used.yp
+ * if a vector is on the stack, it is used
+
+ WARNING: only the first object on the stack is used.
+
+ NOTE:
+ """
+ obj = self.objects[0]
+ p = None
+ if isinstance(obj,Edge):
+ p = obj.endPoint()
+ elif isinstance(obj,Vector):
+ p = obj
+ else:
+ raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj))
+
+ if useLocalCoords:
+ return self.plane.toLocalCoords(p)
+ else:
+ return p
+
+ def rarray(self,xSpacing,ySpacing,xCount,yCount,center=True):
+ """
+ Creates an array of points and pushes them onto the stack.
+ If you want to position the array at another point, create another workplane
+ that is shifted to the position you would like to use as a reference
+
+ :param xSpacing: spacing between points in the x direction ( must be > 0)
+ :param ySpacing: spacing between points in the y direction ( must be > 0)
+ :param xCount: number of points ( > 0 )
+ :param yCount: number of poitns ( > 0 )
+ :param center: if true, the array will be centered at the center of the workplane. if false, the lower
+ left corner will be at the center of the work plane
+ """
+
+ if xSpacing < 1 or ySpacing < 1 or xCount < 1 or yCount < 1:
+ raise ValueError("Spacing and count must be > 0 ")
+
+ lpoints = [] #coordinates relative to bottom left point
+ for x in range(xCount):
+ for y in range(yCount):
+ lpoints.append( (xSpacing*(x), ySpacing*(y)) )
+
+ #shift points down and left relative to origin if requested
+ if center:
+ xc = xSpacing*(xCount-1) * 0.5
+ yc = ySpacing*(yCount-1) * 0.5
+ cpoints = []
+ for p in lpoints:
+ cpoints.append( ( p[0] - xc, p[1] - yc ))
+ lpoints = list(cpoints)
+
+ return self.pushPoints(lpoints)
+
+ def pushPoints(self,pntList):
+ """
+ Pushes a list of points onto the stack as vertices.
+ The points are in the 2-d coordinate space of the workplane face
+
+ :param pntList: a list of points to push onto the stack
+ :type pntList: list of 2-tuples, in *local* coordinates
+ :return: a new workplane with the desired points on the stack.
+
+ A common use is to provide a list of points for a subsequent operation, such as creating circles or holes.
+ This example creates a cube, and then drills three holes through it, based on three points::
+
+ s = Workplane().box(1,1,1).faces(">Z").workplane().pushPoints([(-0.3,0.3),(0.3,0.3),(0,0)])
+ body = s.circle(0.05).cutThruAll()
+
+ Here the circle function operates on all three points, and is then extruded to create three holes.
+ See :py:meth:`circle` for how it works.
+
+ """
+ vecs = []
+ for pnt in pntList:
+ vec = self.plane.toWorldCoords(pnt)
+ vecs.append(vec)
+
+ return self.newObject(vecs)
+
+ def center(self,x,y):
+ """
+ Shift local coordinates to the specified location.
+
+ The location is specified in terms of local coordinates.
+
+ :param float x: the new x location
+ :param float y: the new y location
+ :returns: the workplane object, with the center adjusted.
+
+ The current point is set to the new center.
+ This method is useful to adjust the center point after it has been created automatically on a face,
+ but not where you'd like it to be.
+
+ In this example, we adjust the workplane center to be at the corner of a cube, instead of
+ the center of a face, which is the default::
+
+ #this workplane is centered at x=0.5,y=0.5, the center of the upper face
+ s = Workplane().box(1,1,1).faces(">Z").workplane()
+
+ s.center(-0.5,-0.5) # move the center to the corner
+ t = s.circle(0.25).extrude(0.2)
+ assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner
+
+ The result is a cube with a round boss on the corner
+
+ """
+ "Shift local coordinates to the specified location, according to current coordinates"
+ self.plane.setOrigin2d(x,y)
+ n = self.newObject([self.plane.origin])
+ return n
+
+ def lineTo(self, x, y,forConstruction=False):
+ """
+ Make a line from the current point to the provided point
+
+ :param float x: the x point, in workplane plane coordinates
+ :param float y: the y point, in workplane plane coordinates
+ :return: the Workplane object with the current point at the end of the new line
+
+ see :py:meth:`line` if you want to use relative dimensions to make a line instead.
+
+ """
+ startPoint = self._findFromPoint(False)
+
+ endPoint = self.plane.toWorldCoords((x, y))
+
+ p = Edge.makeLine(startPoint,endPoint)
+
+ if not forConstruction:
+ self._addPendingEdge(p)
+
+ return self.newObject([p])
+
+ #line a specified incremental amount from current point
+ def line(self, xDist, yDist ,forConstruction=False):
+ """
+ Make a line from the current point to the provided point, using
+ dimensions relative to the current point
+
+ :param float xDist: x distance from current point
+ :param float yDist: y distance from current point
+ :return: the workplane object with the current point at the end of the new line
+
+ see :py:meth:`lineTo` if you want to use absolute coordinates to make a line instead.
+
+ """
+ p = self._findFromPoint(True) #return local coordinates
+ return self.lineTo(p.x + xDist, yDist + p.y,forConstruction)
+
+ def vLine(self, distance,forConstruction=False):
+ """
+ Make a vertical line from the current point the provided distance
+
+ :param float distance: (y) distance from current point
+ :return: the workplane object with the current point at the end of the new line
+
+ """
+ return self.line(0, distance,forConstruction)
+
+ def vLineTo(self,yCoord,forConstruction=False):
+ """
+ Make a vertcial line from the current point to the provided y coordinate.
+
+ Useful if it is more convienient to specify the end location rather than distance,
+ as in :py:meth:`vLine`
+
+ :param float yCoord: y coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(p.x,yCoord,forConstruction)
+
+ def hLineTo(self,xCoord,forConstruction=False):
+ """
+ Make a horizontal line from the curren tpoint to the provided x coordinate.
+
+ Useful if it is more convienient to specify the end location rather than distance,
+ as in :py:meth:`hLine`
+
+ :param float xCoord: x coordinate for the end of the line
+ :return: the Workplane object with the current point at the end of the new line
+
+ """
+ p = self._findFromPoint(True)
+ return self.lineTo(xCoord,p.y,forConstruction)
+
+ def hLine(self, distance,forConstruction=False):
+ """
+ Make a horizontal line from the current point the provided distance
+
+ :param float distance: (x) distance from current point
+ :return: the Workplane object with the current point at the end of the new line
+
+ """
+ return self.line(distance, 0,forConstruction)
+
+ #absolute move in current plane, not drawing
+ def moveTo(self, x=0, y=0):
+ """
+ Move to the specified point, without drawing.
+
+ :param x: desired x location, in local coordinates
+ :type x: float, or none for zero
+ :param y: desired y location, in local coorindates
+ :type y: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`move` to do the same thing but using relative dimensions
+ """
+ newCenter = Vector(x,y,0)
+ return self.newObject([self.plane.toWorldCoordinates(newCenter)])
+
+ #relative move in current plane, not drawing
+ def move(self, xDist=0, yDist=0):
+ """
+ Move the specified distance from the current point, without drawing.
+
+ :param xDist: desired x distance, in local coordinates
+ :type xDist: float, or none for zero
+ :param yDist: desired y distance, in local coorindates
+ :type yDist: float, or none for zero.
+
+ Not to be confused with :py:meth:`center`, which moves the center of the entire
+ workplane, this method only moves the current point ( and therefore does not affect objects
+ already drawn ).
+
+ See :py:meth:`moveTo` to do the same thing but using absolute coordinates
+ """
+ p = self._findFromPoint(True)
+ newCenter = p + Vector(xDist,yDist,0)
+ return self.newObject([self.plane.toWorldCoordinates(newCenter)])
+
+
+ def spline(self,listOfXYTuple,forConstruction=False):
+ """
+ Create a spline interpolated through the provided points.
+
+ :param listOfXYTuple: points to interpolate through
+ :type listOfXYTuple: list of 2-tuple
+ :return: a Workplane object with the current point at the end of the spline
+
+ The spline will begin at the current point, and
+ end with the last point in the XY typle list
+
+ This example creates a block with a spline for one side::
+
+ s = Workplane(Plane.XY())
+ sPnts = [
+ (2.75,1.5),
+ (2.5,1.75),
+ (2.0,1.5),
+ (1.5,1.0),
+ (1.0,1.25),
+ (0.5,1.0),
+ (0,1.0)
+ ]
+ r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close()
+ r = r.extrude(0.5)
+
+ *WARNING* It is fairly easy to create a list of points
+ that cannot be correctly interpreted as a spline.
+
+ Future Enhancements:
+ * provide access to control points
+
+ """
+ gstartPoint = self._findFromPoint(False)
+ gEndPoint = self.plane.toWorldCoords(listOfXYTuple[-1])
+
+ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
+ allPoints = [gstartPoint] + vecs
+
+ e = Edge.makeSpline(allPoints)
+
+ if not forConstruction:
+ self._addPendingEdge(e)
+
+ return self.newObject([e])
+
+ def threePointArc(self,point1, point2,forConstruction=False):
+ """
+ Draw an arc from the current point, through point1, and ending at point2
+
+ :param point1: point to draw through
+ :type point1: 2-tuple, in workplane coordinates
+ :param point2: end point for the arc
+ :type point2: 2-tuple, in workplane coordinates
+ :return: a workplane with the current point at the end of the arc
+
+ Future Enhancments:
+ provide a version that allows an arc using relative measures
+ provide a centerpoint arc
+ provide tangent arcs
+
+ """
+
+ gstartPoint = self._findFromPoint(False)
+ gpoint1 = self.plane.toWorldCoords(point1)
+ gpoint2 = self.plane.toWorldCoords(point2)
+
+ arc = Edge.makeThreePointArc(gstartPoint,gpoint1,gpoint2)
+
+ if not forConstruction:
+ self._addPendingEdge(arc)
+
+ return self.newObject([arc])
+
+ def rotateAndCopy(self,matrix):
+ """
+ Makes a copy of all edges on the stack, rotates them according to the
+ provided matrix, and then attempts to consolidate them into a single wire.
+
+ :param matrix: a 4xr transformation matrix, in global coordinates
+ :type matrix: a FreeCAD Base.Matrix object
+ :return: a cadquery object with consolidated wires, and any originals on the stack.
+
+ The most common use case is to create a set of open edges, and then mirror them
+ around either the X or Y axis to complete a closed shape.
+
+ see :py:meth:`mirrorX` and :py:meth:`mirrorY` to mirror about the global X and Y axes
+ see :py:meth:`mirrorX` and for an example
+
+ Future Enhancements:
+ faster implementation: this one transforms 3 times to accomplish the result
+
+
+ """
+
+ #compute rotation matrix ( global --> local --> rotate --> global )
+ #rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
+ rm = self.plane.computeTransform(matrix)
+
+ #convert edges to a wire, if there are pending edges
+ n = self.wire(forConstruction=False)
+
+ #attempt to consolidate wires together.
+ consolidated = n.consolidateWires()
+
+ #ok, mirror all the wires.
+
+ #There might be a better way, but to do this rotation takes 3 steps
+ #transform geometry to local coordinates
+ #then rotate about x
+ #then transform back to global coordiante
+
+ #!!!TODO: needs refactoring. rm.wrapped is a hack, w.transformGeometry is needing a FreeCAD matrix,
+ #so this code is dependent on a freecad matrix even when we dont explicitly import it.
+ #
+ originalWires = consolidated.wires().vals()
+ for w in originalWires:
+ mirrored = w.transformGeometry(rm.wrapped)
+ consolidated.objects.append(mirrored)
+ consolidated._addPendingWire(mirrored)
+
+ #attempt again to consolidate all of the wires
+ c = consolidated.consolidateWires()
+ #c = consolidated
+ return c
+
+ def mirrorY(self):
+ """
+ Mirror entities around the y axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier. This line of code::
+
+ s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25)
+
+ Produces a flat, heart shaped object
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+
+ """
+ tm = Matrix()
+ tm.rotateY(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def mirrorX(self):
+ """
+ Mirror entities around the x axis of the workplane plane.
+
+ :return: a new object with any free edges consolidated into as few wires as possible.
+
+ All free edges are collected into a wire, and then the wire is mirrored,
+ and finally joined into a new wire
+
+ Typically used to make creating wires with symmetry easier.
+
+ Future Enhancements:
+ mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
+
+ """
+ tm = Matrix()
+ tm.rotateX(math.pi)
+ return self.rotateAndCopy(tm)
+
+ def _addPendingEdge(self,edge):
+ """
+ Queues an edge for later combination into a wire.
+
+ :param edge:
+ :return:
+ """
+ self.ctx.pendingEdges.append(edge)
+
+ if self.ctx.firstPoint is None:
+ self.ctx.firstPoint = edge.startPoint()
+
+ def _addPendingWire(self,wire):
+ """
+ Queue a Wire for later extrusion
+
+ Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids.
+
+ but users do not normally care about these distinctions. Users 'think' in terms
+ of edges, and solids.
+
+ CadQuery tracks edges as they are drawn, and automatically combines them into wires
+ when the user does an operation that needs it.
+
+ Similarly, cadQuery tracks pending wires, and automaticlaly combines them into faces
+ when necessary to make a solid.
+ """
+ self.ctx.pendingWires.append(wire)
+
+
+ def consolidateWires(self):
+ """
+ Attempt to consolidate wires on the stack into a single.
+ If possible, a new object with the results are returned.
+ if not possible, the wires remain separated
+
+ FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly somtimes
+ Additionally, it has a bug where a profile compose of two wires ( rathre than one )
+ also does not work properly
+
+ together these are a real problem.
+ """
+ wires = self.wires().vals()
+ if len(wires) < 2:
+ return self
+
+ #TODO: this makes the assumption that either all wires could be combined, or none.
+ #in reality trying each combination of wires is probably not reasonable anyway
+ w = Wire.combine(wires)
+
+ #ok this is a little tricky. if we consolidate wires, we have to actually
+ #modify the pendingWires collection to remove the original ones, and replace them
+ #with the consolidate done
+ #since we are already assuming that all wires could be consolidated, its easy, we just
+ #clear the pending wire list
+ r = self.newObject([w])
+ r.ctx.pendingWires = []
+ r._addPendingWire(w)
+ return r
+
+
+
+ def wire(self,forConstruction=False):
+ """
+ Returns a CQ object with all pending edges connected into a wire.
+
+ All edges on the stack that can be combined will be combined into a single wire object,
+ and other objects will remain on the stack unmodified
+
+ :param forConstruction: whether the wire should be used to make a solid, or if it is just for reference
+ :type forConstruction: boolean. true if the object is only for reference
+
+ This method is primarily of use to plugin developers making utilites for 2-d construction. This method
+ shoudl be called when a user operation implies that 2-d construction is finished, and we are ready to
+ begin working in 3d
+
+ SEE '2-d construction concepts' for a more detailed explanation of how cadquery handles edges, wires, etc
+
+ Any non edges will still remain.
+ """
+
+ edges = self.ctx.pendingEdges
+
+ #do not consolidate if there are no free edges
+ if len(edges) == 0:
+ return self
+
+ self.ctx.pendingEdges = []
+
+ others = []
+ for e in self.objects:
+ if type(e) != Edge:
+ others.append(e)
+
+
+ w = Wire.assembleEdges(edges)
+ if not forConstruction:
+ self._addPendingWire(w)
+
+ return self.newObject(others + [w])
+
+ def each(self,callBackFunction,useLocalCoordinates=False):
+ """
+ runs the provided function on each value in the stack, and collects the return values into a new CQ
+ object.
+
+ Special note: a newly created workplane always has its center point as its only stack item
+
+ :param callBackFunction: the function to call for each item on the current stack.
+ :param useLocalCoordinates: should values be converted from local coordinates first?
+ :type useLocalCoordinates: boolean
+
+ The callback function must accept one argument, which is the item on the stack, and return
+ one object, which is collected. If the function returns None, nothing is added to the stack.
+ The object passed into the callBackFunction is potentially transformed to local coordinates, if
+ useLocalCoordinates is true
+
+ useLocalCoordinates is very useful for plugin developers.
+
+ If false, the callback function is assumed to be working in global coordinates. Objects created are added
+ as-is, and objects passed into the function are sent in using global coordinates
+
+ If true, the calling function is assumed to be working in local coordinates. Objects are transformed
+ to local coordinates before they are passed into the callback method, and result objects are transformed
+ to global coorindates after they are returned.
+
+ This allows plugin developers to create objects in local coordinates, without worrying
+ about the fact that the working plane is different than the global coordinate system.
+
+
+ TODO: wrapper object for Wire will clean up forConstruction flag everywhere
+
+ """
+ results = []
+ for obj in self.objects:
+
+ if useLocalCoordinates:
+ #TODO: this needs to work for all types of objects, not just vectors!
+ r = callBackFunction(self.plane.toLocalCoords(obj))
+ r = r.transformShape(self.plane.rG)
+ else:
+ r = callBackFunction(obj)
+
+
+ if type(r) == Wire:
+ if not r.forConstruction:
+ self._addPendingWire(r)
+
+ results.append ( r )
+
+
+ return self.newObject(results)
+
+ def eachpoint(self,callbackFunction, useLocalCoordinates=False):
+ """
+ Same as each(), except each item on the stack is converted into a point before it
+ is passed into the callback function.
+
+ :return: CadQuery object which contains a list of vectors (points ) on its stack.
+
+ :param useLocalCoordinates: should points be in local or global coordinates
+ :type useLocalCoordinates: boolean
+
+ The resulting object has a point on the stack for each object on the original stack.
+ Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted
+ to a point by using their center of mass.
+
+ If the stack has zero length, a single point is returned, which is the center of the current
+ workplane/coordinate system
+
+ """
+ #convert stack to a list of points
+ pnts = []
+ if len(self.objects) == 0:
+ #nothing on the stack. here, we'll assume we should operate with the
+ #origin as the context point
+ pnts.append(self.plane.origin)
+ else:
+
+ for v in self.objects:
+ pnts.append(v.Center())
+
+ return self.newObject(pnts).each(callbackFunction,useLocalCoordinates )
+
+
+ #make a rectangle
+ def rect(self,xLen,yLen,centered=True,forConstruction=False):
+ """
+ Make a rectangle for each item on the stack.
+
+ :param xLen: length in xDirection ( in workplane coordinates )
+ :type xLen: float > 0
+ :param yLen: length in yDirection ( in workplane coordinates )
+ :type yLen: float > 0
+ :param boolean centered: true if the rect is centered on the reference point, false if the lower-left is on the reference point
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating part geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+
+ """
+ def makeRectangleWire(pnt):
+ #here pnt is in local coordinates due to useLocalCoords=True
+ (xc,yc,zc) = pnt.toTuple()
+ if centered:
+ p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0,0) )
+ p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0,0) )
+ p3 = pnt.add(Vector(xLen/2.0, yLen/2.0,0) )
+ p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0,0) )
+ else:
+ p1 = pnt
+ p2 = pnt.add(Vector(xLen,0,0))
+ p3 = pnt.add(Vector( xLen,yLen,0 ))
+ p4 = pnt.add(Vector(0,yLen,0))
+
+ w = Wire.makePolygon([p1,p2,p3,p4,p1],forConstruction)
+ return w
+ #return Part.makePolygon([p1,p2,p3,p4,p1])
+
+ return self.eachpoint(makeRectangleWire,True)
+
+ #circle from current point
+ def circle(self,radius,forConstruction=False):
+ """
+ Make a circle for each item on the stack.
+
+ :param radius: radius of the circle
+ :type radius: float > 0
+ :param forConstruction: should the new wires be reference geometry only?
+ :type forConstruction: true if the wires are for reference, false if they are creating part geometry
+ :return: a new CQ object with the created wires on the stack
+
+ A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
+
+ s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
+
+ Creates 4 circles at the corners of a square centered on the origin. Another common case is to use
+ successive circle() calls to create concentric circles. This works because the center of a circle
+ is its reference point::
+
+ s = Workplane().circle(2.0).circle(1.0)
+
+ Creates two concentric circles, which when extruded will form a ring.
+
+ Future Enhancements:
+ better way to handle forConstruction
+ project points not in the workplane plane onto the workplane plane
+
+ """
+ def makeCircleWire(obj):
+ cir = Wire.makeCircle(radius,obj,Vector(0,0,1))
+ cir.forConstruction = forConstruction
+ return cir
+
+ return self.eachpoint(makeCircleWire,useLocalCoordinates=True)
+
+ def polygon(self,nSides,diameter):
+ """
+ Creates a polygon incribed in a circle of the specified diamter for each point on the stack
+
+ The first vertex is always oriented in the x direction.
+
+ :param nSides: number of sides, must be > 3
+ :param diameter: the size of the circle the polygon is incribed into
+ :return: a polygon wire
+
+
+ """
+ def _makePolygon(center):
+ #pnt is a vector in local coordinates
+ angle = 2.0 *math.pi / nSides
+ pnts = []
+ for i in range(nSides+1):
+ pnts.append( center + Vector((diameter / 2.0 * math.cos(angle*i)),(diameter / 2.0 * math.sin(angle*i)),0))
+ return Wire.makePolygon(pnts)
+
+ return self.eachpoint(_makePolygon,True)
+
+ def polyline(self,listOfXYTuple,forConstruction=False):
+ """
+ Create a polyline from a list of points
+
+ :param listOfXYTuple: a list of points in Workplane coordinates
+ :type listOfXYTuple: list of 2-tuples
+ :param forConstruction: should the new wire be reference geometry only?
+ :type forConstruction: true if the wire is for reference, false if they are creating part geometry
+ :return: a new CQ object with the new wire on the stack
+
+ *NOTE* most commonly, the resulting wire should be closed.
+
+ Future Enhacement:
+ This should probably yield a list of edges, not a wire, so that
+ it is possible to combine a polyline with other edges and arcs
+ """
+ vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
+ w = Wire.makePolygon(vecs)
+ if not forConstruction:
+ self._addPendingWire(w)
+
+ return self.newObject([w])
+
+ #finish a set of lines.
+ #
+ def close(self):
+ """
+ End 2-d construction, and attempt to build a closed wire.
+
+ :return: a CQ object with a completed wire on the stack, if possible.
+
+ After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary
+ to convert the edges produced by these into one or more wires.
+
+ When a set of edges is closed, cadQuery assumes it is safe to build the group of edges
+ into a wire. This example builds a simple triangular prism::
+
+ s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2)
+
+ """
+ self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y)
+ return self.wire()
+
+ def largestDimension(self):
+ """
+ Finds the largest dimension in the stack.
+ Used internally to create thru features, this is how you can compute
+ how long or wide a feature must be to make sure to cut through all of the material
+ :return:
+ """
+ #TODO: this implementation is naive and returns the dims of the first solid... most of
+ #the time this works. but a stronger implementation would be to search all solids.
+ s = self.findSolid()
+ if s:
+ return s.BoundingBox().DiagonalLength * 5.0
+ else:
+ return 1000000
+
+ def cutEach(self,fcn,useLocalCoords=False):
+ """
+ Evaluates the provided function at each point on the stack ( ie, eachpoint )
+ and then cuts the result from the context solid.
+ :param function: a function suitable for use in the eachpoint method: ie, that accepts a vector
+ :param useLocalCoords: same as for :py:meth:`eachpoint`
+ :return: a CQ object that contains the resulting solid
+ :raises: an error if there is not a context solid to cut from
+ """
+ ctxSolid = self.findSolid()
+ if ctxSolid is None:
+ raise ValueError ("Must have a solid in the chain to cut from!")
+
+ #will contain all of the counterbores as a single compound
+ results = self.eachpoint(fcn,useLocalCoords).vals()
+ s = ctxSolid
+ for cb in results:
+ s = s.cut(cb)
+
+ ctxSolid.wrapped = s.wrapped
+ return self.newObject([s])
+
+ #but parameter list is different so a simple function pointer wont work
+ def cboreHole(self,diameter,cboreDiameter,cboreDepth,depth=None):
+ """
+ Makes a counterbored hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param cboreDiameter: the diameter of the cbore
+ :type cboreDiameter: float > 0 and > diameter
+ :param cboreDepth: depth of the counterbore
+ :type cboreDepth: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane plane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().cboreHole(0.125, 0.25,0.125,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. Counterbored holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cskHole` to make countersinks instead of counterbores
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCbore(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0,0,-1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
+
+ #add the counter bore
+ cbore = Solid.makeCylinder(cboreDiameter/2.0,cboreDepth,center,boreDir)
+ r = hole.fuse(cbore)
+ return r
+
+ return self.cutEach(_makeCbore,True)
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def cskHole(self,diameter, cskDiameter,cskAngle,depth=None):
+ """
+ Makes a countersunk hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param cskDiameter: the diameter of the countersink
+ :type cskDiameter: float > 0 and > diameter
+ :param cskAngle: angle of the countersink, in degrees ( 82 is common )
+ :type cskAngle: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().cskHole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` to make counterbores instead of countersinks
+ """
+
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeCsk(center):
+ #center is in local coordinates
+
+ boreDir = Vector(0,0,-1)
+
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coords!
+ r = cskDiameter / 2.0
+ h = r / math.tan(math.radians(cskAngle / 2.0))
+ csk = Solid.makeCone(r,0.0,h,center,boreDir)
+ r = hole.fuse(csk)
+ return r
+
+ return self.cutEach(_makeCsk,True)
+
+
+ #TODO: almost all code duplicated!
+ #but parameter list is different so a simple function pointer wont work
+ def hole(self,diameter,depth=None):
+ """
+ Makes a hole for each item on the stack.
+
+ :param diameter: the diameter of the hole
+ :type diamter: float > 0
+ :param depth: the depth of the hole
+ :type depth: float > 0 or None to drill thru the entire part.
+
+ The surface of the hole is at the current workplane.
+
+ One hole is created for each item on the stack. A very common use case is to use a
+ construction rectangle to define the centers of a set of holes, like so::
+
+ s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
+ .vertices().hole(0.125, 0.25,82,depth=None)
+
+ This sample creates a plate with a set of holes at the corners.
+
+ **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
+ to create, but are quite easily defined by users.
+
+ see :py:meth:`cboreHole` and :py:meth:`cskHole` to make counterbores or countersinks
+ """
+ if depth is None:
+ depth = self.largestDimension()
+
+ def _makeHole(center):
+ """
+ Makes a single hole with counterbore at the supplied point
+ returns a solid suitable for subtraction
+ pnt is in local coordinates
+ """
+ boreDir = Vector(0,0,-1)
+ #first make the hole
+ hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
+ return hole
+
+ return self.cutEach(_makeHole,True)
+
+ #TODO: duplicated code with _extrude and extrude
+ def twistExtrude(self,distance,angleDegrees,combine=True):
+ """
+ Extrudes a wire in the direction normal to the plane, but also twists by the specified angle over the
+ length of the extrusion
+
+ The center point of the rotation will be the center of the workplane
+
+ See extrude for more details, since this method is the same except for the the addition of the angle.
+ in fact, if angle=0, the result is the same as a linear extrude.
+
+ **NOTE** This method can create complex calculations, so be careful using it with complex geometries
+
+ :param distance: the distance to extrude normal to the workplane
+ :param angle: angline ( in degrees) to rotate through the extrusion
+ :param boolean combine: True to combine the resulting solid with parent solids if found.
+ :return: a CQ object with the resulting solid selected.
+
+ """
+ #group wires together into faces based on which ones are inside the others
+ #result is a list of lists
+ wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires),self.plane,[])
+
+ self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+ #one would think that fusing faces into a compound and then extruding would work,
+ #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
+ #but then cutting it from the main solid fails with BRep_NotDone.
+ #the work around is to extrude each and then join the resulting solids, which seems to work
+
+ #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
+ r = None
+ for ws in wireSets:
+ thisObj = Solid.extrudeLinearWithRotation(ws[0],ws[1:],self.plane.origin, eDir,angleDegrees)
+ if r is None:
+ r = thisObj
+ else:
+ r = r.fuse(thisObj)
+
+ if combine:
+ return self._combineWithBase(r)
+ else:
+ return self.newObject([r])
+
+ def extrude(self,distance,combine=True):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic solid.
+
+ :param distance: the distance to extrude, normal to the workplane plane
+ :type distance: float, negative means opposite the normal direction
+ :param boolean combine: True to combine the resulting solid with parent solids if found.
+ :return: a CQ object with the resulting solid selected.
+
+ extrude always *adds* material to a part.
+
+ The returned object is always a CQ object, and depends on wither combine is True, and
+ whether a context solid is already defined:
+
+ * if combine is False, the new value is pushed onto the stack.
+ * if combine is true, the value is combined with the context solid if it exists,
+ and the resulting solid becomes the new context solid.
+
+ FutureEnhancement:
+ Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane
+ extrude to surface. this is quite tricky since the surface selected may not be planar
+ """
+ r = self._extrude(distance) #returns a Solid ( or a compound if there were multiple )
+ if combine:
+ return self._combineWithBase(r)
+ else:
+ return self.newObject([r])
+
+ def _combineWithBase2(self,obj):
+ """
+ Combines the provided object with the base solid, if one can be found.
+ :param obj:
+ :return: a new object that represents the result of combining the base object with obj,
+ or obj if one could not be found
+
+ """
+ baseSolid = self.findSolid(searchParents=True)
+ r = obj
+ if baseSolid is not None:
+ r = baseSolid.fuse(obj)
+ baseSolid.wrapped = r.wrapped
+
+ return self.newObject([r])
+
+ def _combineWithBase(self,obj):
+ """
+ Combines the provided object with the base solid, if one can be found.
+ :param obj:
+ :return: a new object that represents the result of combining the base object with obj,
+ or obj if one could not be found
+
+ """
+ baseSolid = self.findSolid(searchParents=True)
+ r = obj
+ if baseSolid is not None:
+ r = baseSolid.fuse(obj)
+ baseSolid.wrapped = r.wrapped
+
+ return self.newObject([r])
+
+ def combine(self):
+ """
+ Attempts to combine all of the items on the items on the stack into a single item.
+ WARNING: all of the items must be of the same type!
+
+ :raises: ValueError if there are no items on the stack, or if they cannot be combined
+ :return: a CQ object with the resulting object selected
+ """
+ items = list(self.objects)
+ s = items.pop(0)
+ for ss in items:
+ s = s.fuse(ss)
+
+ return self.newObject([s])
+
+ def union(self,toUnion=None,combine=True):
+ """
+ Unions all of the items on the stack of toUnion with the current solid.
+ If there is no current solid, the items in toUnion are unioned together.
+ if combine=True, the result and the original are updated to point to the new object
+ if combine=False, the result will be on the stack, but the original is unmodified
+
+
+ :param toUnion:
+ :type toUnion: a solid object, or a CQ object having a solid,
+ :raises: ValueError if there is no solid to add to in the chain
+ :return: a CQ object with the resulting object selected
+ """
+
+ #first collect all of the items together
+ if type(toUnion) == CQ or type(toUnion) == Workplane:
+ solids = toUnion.solids().vals()
+ if len(solids) < 1 :
+ raise ValueError("CQ object must have at least one solid on the stack to union!")
+ newS = solids.pop(0)
+ for s in solids:
+ newS = newS.fuse(s)
+ elif type(toUnion) == Solid:
+ newS = toUnion
+ else:
+ raise ValueError("Cannot union Type '%s' " % str(type(toUnion)))
+
+ #now combine with existing solid, if there is one
+ solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from
+ if combine and solidRef is not None:
+ t = solidRef.fuse(newS)
+ solidRef.wrapped = newS.wrapped
+ return self.newObject([t])
+ else:
+ return self.newObject([newS])
+
+ def cut(self,toCut,combine=True):
+ """
+ Cuts the provided solid from the current solid, IE, perform a solid subtraction
+
+ if combine=True, the result and the original are updated to point to the new object
+ if combine=False, the result will be on the stack, but the original is unmodified
+
+ :param toCut: object to cut
+ :type toCut: a solid object, or a CQ object having a solid,
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ """
+
+ solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from
+
+ if solidRef is None:
+ raise ValueError("Cannot find solid to cut from!!!")
+ solidToCut = None
+ if type(toCut) == CQ or type(toCut) == Workplane:
+ solidToCut = toCut.val()
+ elif type(toCut) == Solid:
+ solidToCut = toCut
+ else:
+ raise ValueError("Cannot cut Type '%s' " % str(type(toCut)))
+
+ newS = solidRef.cut(solidToCut)
+ if combine:
+ solidRef.wrapped = newS.wrapped
+ return self.newObject([newS])
+
+
+ def cutBlind(self,distanceToCut):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material from.
+ cutBlind always removes material from a part.
+
+ :param distanceToCut: distance to extrude before cutting
+ :type distanceToCut: float, >0 means in the positive direction of the workplane normal, <0 means in the negative direction
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutThruAll` to cut material from the entire part
+
+ Future Enhancements:
+ Cut Up to Surface
+ """
+ #first, make the object
+ toCut = self._extrude(distanceToCut)
+
+ #now find a solid in the chain
+
+ solidRef = self.findSolid()
+
+ s= solidRef.cut(toCut)
+ solidRef.wrapped = s.wrapped
+ return self.newObject([s])
+
+ def cutThruAll(self,positive=False):
+ """
+ Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
+
+ Similar to extrude, except that a solid in the parent chain is required to remove material from.
+ cutThruAll always removes material from a part.
+
+ :param boolean positive: True to cut in the positive direction, false to cut in the negative direction
+ :raises: ValueError if there is no solid to subtract from in the chain
+ :return: a CQ object with the resulting object selected
+
+ see :py:meth:`cutBlind` to cut material to a limited depth
+
+ """
+ maxDim = self.largestDimension()
+ if not positive:
+ maxDim *= (-1.0)
+
+ return self.cutBlind(maxDim)
+
+
+ def loft(self,filled=True,combine=True):
+ """
+ Make a lofted solid, through the set of wires.
+ :return:
+ """
+ wiresToLoft = self.ctx.pendingWires
+ self.ctx.pendingWires = []
+
+ r = Solid.makeLoft(wiresToLoft)
+
+ if combine:
+ parentSolid = self.findSolid(searchStack=False,searchParents=True)
+ if parentSolid is not None:
+ r = parentSolid.fuse(r)
+ parentSolid.wrapped = r.wrapped
+
+ return self.newObject([r])
+
+ def _extrude(self,distance):
+ """
+ Make a prismatic solid from the existing set of pending wires.
+
+ :param distance: distance to extrude
+ :return: a FreeCAD solid, suitable for boolean operations.
+
+ This method is a utility method, primarily for plugin and internal use.
+ It is the basis for cutBlind,extrude,cutThruAll, and all similar methods.
+
+ Future Enhancements:
+ extrude along a profile ( sweep )
+ """
+
+ #group wires together into faces based on which ones are inside the others
+ #result is a list of lists
+ s = time.time()
+ wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires),self.plane,[])
+ #print "sorted wires in %d sec" % ( time.time() - s )
+ self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
+
+ #compute extrusion vector and extrude
+ eDir = self.plane.zDir.multiply(distance)
+
+
+ #one would think that fusing faces into a compound and then extruding would work,
+ #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
+ #but then cutting it from the main solid fails with BRep_NotDone.
+ #the work around is to extrude each and then join the resulting solids, which seems to work
+
+ #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
+
+ # IMPORTANT NOTE: OCC is slow slow slow in boolean operations. So you do NOT want to fuse each item to
+ # another and save the result-- instead, you want to combine all of the new items into a compound, and fuse
+ # them together!!!
+ """
+ r = None
+ for ws in wireSets:
+ thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
+ if r is None:
+ r = thisObj
+ else:
+ s = time.time()
+ r = r.fuse(thisObj)
+ print "Fused in %0.3f sec" % ( time.time() - s )
+ return r
+ """
+
+ toFuse = []
+ for ws in wireSets:
+ thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
+ toFuse.append(thisObj)
+
+ return Compound.makeCompound(toFuse)
+
+
+ def box(self,length,width,height,centered=(True,True,True),combine=True):
+ """
+ Return a 3d box with specified dimensions for each object on the stack.
+
+ :param length: box size in X direction
+ :type length: float > 0
+ :param width: box size in Y direction
+ :type width: float > 0
+ :param height: box size in Z direction
+ :type height: float > 0
+ :param centered: should the box be centered, or should reference point be at the lower bound of the range?
+ :param combine: should the results be combined with other solids on the stack ( and each other)?
+ :type combine: true to combine shapes, false otherwise.
+
+ Centered is a tuple that describes whether the box should be centered on the x,y, and z axes. If true,
+ the box is centered on the respective axis relative to the workplane origin, if false, the workplane center
+ will represent the lower bound of the resulting box
+
+ one box is created for each item on the current stack. If no items are on the stack, one box using
+ the current workplane center is created.
+
+ If combine is true, the result will be a single object on the stack:
+ if a solid was found in the chain, the result is that solid with all boxes produced fused onto it
+ otherwise, the result is the combination of all the produced boxes
+
+ if combine is false, the result will be a list of the boxes produced
+
+ Most often boxes form the basis for a part::
+
+ #make a single box with lower left corner at origin
+ s = Workplane().box(1,2,3,centered=(False,False,False)
+
+ But sometimes it is useful to create an array of them:
+
+ #create 4 small square bumps on a larger base plate:
+ s = Workplane().box(4,4,0.5).faces(">Z").workplane()\
+ .rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True)
+
+ """
+
+ def _makebox(pnt):
+
+ #(xp,yp,zp) = self.plane.toLocalCoords(pnt)
+ (xp,yp,zp) = pnt.toTuple()
+ if centered[0]:
+ xp = xp-(length/2.0)
+ if centered[1]:
+ yp = yp-(width/2.0)
+ if centered[2]:
+ zp = zp-(height/2.0)
+
+ return Solid.makeBox(length,width,height,Vector(xp,yp,zp))
+
+ boxes = self.eachpoint(_makebox,True)
+
+ #if combination is not desired, just return the created boxes
+ if not combine:
+ return boxes
+ else:
+ #combine everything
+ return self.union(boxes)
+
diff --git a/cadquery/README.txt b/cadquery/README.txt
new file mode 100644
index 0000000..ab8dc7e
--- /dev/null
+++ b/cadquery/README.txt
@@ -0,0 +1,8 @@
+***
+Core CadQuery implementation.
+
+No files should depend on or import FreeCAD , pythonOCC, or other CAD Kernel libraries!!!
+Dependencies should be on the classes provided by implementation packages, which in turn
+can depend on CAD libraries.
+
+***
\ No newline at end of file
diff --git a/cadquery/__init__.py b/cadquery/__init__.py
index c3354a6..5ac20a7 100644
--- a/cadquery/__init__.py
+++ b/cadquery/__init__.py
@@ -19,20 +19,20 @@
#these items point to the freecad implementation
-from .freecad_impl.geom import Plane,BoundBox,Vector
-from .freecad_impl.shapes import Shape,Vertex,Edge,Wire,Solid,Shell,Compound
-from .freecad_impl.exporters import SvgExporter, AmfExporter, JsonExporter
+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
#these items are the common implementation
-from .CQ import CQ
-from .workplane import Workplane
-from . import plugins
+
+#the order of these matter
from . import selectors
+from .CQ import CQ,CQContext,Workplane
+
__all__ = [
- 'CQ','Workplane','plugins','selectors','Plane','BoundBox',
- 'Shape','Vertex','Edge','Wire','Solid','Shell','Compound',
- 'SvgExporter','AmfExporter','JsonExporter',
+ 'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder',
+ 'Shape','Vertex','Edge','Wire','Solid','Shell','Compound','exporters',
'plugins'
]
diff --git a/cadquery/freecad_impl/README.txt b/cadquery/freecad_impl/README.txt
new file mode 100644
index 0000000..34ea788
--- /dev/null
+++ b/cadquery/freecad_impl/README.txt
@@ -0,0 +1,3 @@
+It is ok for files in this directory to import FreeCAD, FreeCAD.Base, and FreeCAD.Part.
+
+Other modules should _not_ depend on FreeCAD
\ No newline at end of file
diff --git a/cadquery/freecad_impl/exporters.py b/cadquery/freecad_impl/exporters.py
index 0a01498..be2da12 100644
--- a/cadquery/freecad_impl/exporters.py
+++ b/cadquery/freecad_impl/exporters.py
@@ -17,6 +17,9 @@
License along with this library; If not, see
"""
+import FreeCAD
+from FreeCAD import Drawing
+
try:
import xml.etree.cElementTree as ET
except ImportError:
@@ -54,15 +57,8 @@ def guessUnitOfMeasure(shape):
return UNITS.MM
-class Exporter(object):
-
- def export(self):
- """
- return a string representing the model exported in the specified format
- """
- raise NotImplementedError()
-class AmfExporter(Exporter):
+class AmfExporter(object):
def __init__(self,tessellation):
self.units = "mm"
@@ -105,7 +101,7 @@ class AmfExporter(Exporter):
three.js JSON object notation
https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
"""
-class JsonExporter(Exporter):
+class JsonExporter(object):
def __init__(self):
self.vertices = [];
@@ -135,10 +131,6 @@ class JsonExporter(Exporter):
'nFaces' : self.nFaces
};
-class SvgExporter(Exporter):
-
- def export(self):
- pass
def getPaths(freeCadSVG):
"""
@@ -240,11 +232,15 @@ def getSVG(shape,opts=None):
#)
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)
+
+ svg = getSVG(shape.val().wrapped)
f = open(fileName,'w')
f.write(svg)
f.close()
diff --git a/cadquery/freecad_impl/geom.py b/cadquery/freecad_impl/geom.py
index 9de4ae5..4c193ac 100644
--- a/cadquery/freecad_impl/geom.py
+++ b/cadquery/freecad_impl/geom.py
@@ -1,20 +1,20 @@
"""
- Copyright (C) 2011-2013 Parametric Products Intellectual Holdings, LLC
+ Copyright (C) 2011-2013 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.
+ 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.
+ 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
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; If not, see
"""
import math,sys
@@ -48,33 +48,6 @@ def sortWiresByBuildOrder(wireList,plane,result=[]):
return result
-def sortWiresByBuildOrderOld(wireList,plane,result=[]):
- """
- Tries to determine how wires should be combined into faces.
- Assume:
- The wires make up one or more faces, which could have 'holes'
- Outer wires are listed ahead of inner wires
- there are no wires inside wires inside wires ( IE, islands -- we can deal with that later on )
- none of the wires are construction wires
- Compute:
- one or more sets of wires, with the outer wire listed first, and inner ones
- Returns, list of lists.
- """
- outerWire = wireList.pop(0)
-
- remainingWires = list(wireList)
- childWires = []
- for w in wireList:
- if plane.isWireInside(outerWire,w):
- childWires.append(remainingWires.pop(0))
- else:
- #doesnt match, assume this wire is a new outer
- result.append([outerWire] + childWires)
- return sortWiresByBuildOrder(remainingWires,plane,result)
-
- result.append([outerWire] + childWires)
- return result
-
class Vector(object):
"""
Create a 3-dimensional vector
@@ -212,6 +185,25 @@ class Vector(object):
def __eq__(self,other):
return self.wrapped.__eq__(other)
+class Matrix:
+ """
+ A 3d , 4x4 transformation matrix.
+
+ Used to move geometry in space.
+ """
+ def __init__(self,matrix=None):
+ if matrix == None:
+ self.wrapped = FreeCAD.Base.Matrix()
+ else:
+ self.wrapped = matrix
+
+ def rotateX(self,angle):
+ self.wrapped.rotateX(angle)
+
+ def rotateY(self,angle):
+ self.wrapped.rotateY(angle)
+
+
class Plane:
"""
A 2d coordinate system in space, with the x-y axes on the a plane, and a particular point as the origin.
@@ -329,6 +321,7 @@ class Plane:
self.setOrigin3d(origin)
+
def setOrigin3d(self,originVector):
"""
Move the origin of the plane, leaving its orientation and xDirection unchanged.
@@ -417,6 +410,7 @@ class Plane:
else:
raise ValueError("Dont know how to convert type %s to local coordinates" % str(type(obj)))
+
def toWorldCoords(self, tuplePoint):
"""
Convert a point in local coordinates to global coordinates.
@@ -433,6 +427,7 @@ class Plane:
v = Vector(tuplePoint[0],tuplePoint[1],tuplePoint[2])
return Vector(self.rG.multiply(v.wrapped))
+
def rotated(self,rotate=Vector(0,0,0)):
"""
returns a copy of this plane, rotated about the specified axes, as measured from horizontal
@@ -453,7 +448,7 @@ class Plane:
rotate = rotate.multiply(math.pi / 180.0 )
#compute rotation matrix
- m = Base.Matrix()
+ m = FreeCAD.Base.Matrix()
m.rotateX(rotate.x)
m.rotateY(rotate.y)
m.rotateZ(rotate.z)
@@ -473,7 +468,7 @@ class Plane:
#ok i will be really honest-- i cannot understand exactly why this works
#something bout the order of the transaltion and the rotation.
# the double-inverting is strange, and i dont understand it.
- r = Base.Matrix()
+ 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 )
@@ -484,7 +479,14 @@ class Plane:
(invR.A14,invR.A24,invR.A34) = (self.origin.x,self.origin.y,self.origin.z)
( self.rG,self.fG ) = ( invR,invR.inverse() )
-
+
+ def computeTransform(self,tMatrix):
+ """
+ Computes the 2-d projection of the supplied matrix
+ """
+
+ rm = self.fG.multiply(tMatrix.wrapped).multiply(self.rG)
+ return Matrix(rm)
class BoundBox(object):
"A BoundingBox for an object or set of objects. Wraps the FreeCAD one"
diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py
index a41f342..bcfc4c1 100644
--- a/cadquery/freecad_impl/shapes.py
+++ b/cadquery/freecad_impl/shapes.py
@@ -47,7 +47,7 @@
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
+from cadquery import Vector,BoundBox
import FreeCAD
import FreeCAD.Part
@@ -65,7 +65,7 @@ class Shape(object):
def cast(cls,obj,forConstruction = False):
"Returns the right type of wrapper, given a FreeCAD object"
s = obj.ShapeType
- if type(obj) == Base.Vector:
+ if type(obj) == FreeCAD.Base.Vector:
return Vector(obj)
tr = None
@@ -99,7 +99,9 @@ class Shape(object):
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)
@@ -254,6 +256,7 @@ class Shape(object):
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
"""
@@ -265,6 +268,8 @@ class Shape(object):
def transformGeometry(self,tMatrix):
"""
+ tMatrix is a matrix object.
+
returns a copy of the object, but with geometry transformed insetad of just
rotated.
@@ -311,9 +316,9 @@ class Edge(Shape):
#self.endPoint = None
self.edgetypes= {
- Part.Line : 'LINE',
- Part.ArcOfCircle : 'ARC',
- Part.Circle : 'CIRCLE'
+ FreeCAD.Part.Line : 'LINE',
+ FreeCAD.Part.ArcOfCircle : 'ARC',
+ FreeCAD.Part.Circle : 'CIRCLE'
}
def geomType(self):
@@ -363,7 +368,7 @@ class Edge(Shape):
@classmethod
def makeCircle(cls,radius,pnt=(0,0,0),dir=(0,0,1),angle1=360.0,angle2=360):
- return Edge(Part.makeCircle(radius,toVector(pnt),toVector(dir),angle1,angle2))
+ return Edge(FreeCAD.Part.makeCircle(radius,toVector(pnt),toVector(dir),angle1,angle2))
@classmethod
def makeSpline(cls,listOfVector):
@@ -375,7 +380,7 @@ class Edge(Shape):
"""
vecs = [v.wrapped for v in listOfVector]
- spline = Part.BSplineCurve()
+ spline = FreeCAD.Part.BSplineCurve()
spline.interpolate(vecs,False)
return Edge(spline.toShape())
@@ -389,7 +394,7 @@ class Edge(Shape):
:param v3: end vector
:return: an edge object through the three points
"""
- arc = Part.Arc(v1.wrapped,v2.wrapped,v3.wrapped)
+ arc = FreeCAD.Part.Arc(v1.wrapped,v2.wrapped,v3.wrapped)
e = Edge(arc.toShape())
return e #arcane and undocumented, this creates an Edge object
@@ -401,7 +406,7 @@ class Edge(Shape):
:param v2: Vector that represents the second point
:return: A linear edge between the two provided points
"""
- return Edge(Part.makeLine(v1.toTuple(),v2.toTuple() ))
+ return Edge(FreeCAD.Part.makeLine(v1.toTuple(),v2.toTuple() ))
class Wire(Shape):
@@ -420,7 +425,7 @@ class Wire(Shape):
:param listOfWires:
:return:
"""
- return Shape.cast(Part.Wire([w.wrapped for w in listOfWires]))
+ return Shape.cast(FreeCAD.Part.Wire([w.wrapped for w in listOfWires]))
@classmethod
def assembleEdges(cls,listOfEdges):
@@ -432,7 +437,7 @@ class Wire(Shape):
"""
fCEdges = [a.wrapped for a in listOfEdges]
- wa = Wire( Part.Wire(fCEdges) )
+ wa = Wire( FreeCAD.Part.Wire(fCEdges) )
return wa
@classmethod
@@ -444,13 +449,13 @@ class Wire(Shape):
:param normal: vector representing the direction of the plane the circle should lie in
:return:
"""
- w = Wire(Part.Wire([Part.makeCircle(radius,center.wrapped,normal.wrapped)]))
+ w = Wire(FreeCAD.Part.Wire([FreeCAD.Part.makeCircle(radius,center.wrapped,normal.wrapped)]))
return w
@classmethod
def makePolygon(cls,listOfVertices,forConstruction=False):
#convert list of tuples into Vectors.
- w = Wire(Part.makePolygon([i.wrapped for i in listOfVertices]))
+ w = Wire(FreeCAD.Part.makePolygon([i.wrapped for i in listOfVertices]))
w.forConstruction = forConstruction
return w
@@ -461,7 +466,7 @@ class Wire(Shape):
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(Part.makeHelix(pitch,height,radius,angle))
+ return Wire(FreeCAD.Part.makeHelix(pitch,height,radius,angle))
class Face(Shape):
@@ -473,9 +478,9 @@ class Face(Shape):
self.facetypes = {
#TODO: bezier,bspline etc
- Part.Plane : 'PLANE',
- Part.Sphere : 'SPHERE',
- Part.Cone : 'CONE'
+ FreeCAD.Part.Plane : 'PLANE',
+ FreeCAD.Part.Sphere : 'SPHERE',
+ FreeCAD.Part.Cone : 'CONE'
}
def geomType(self):
@@ -501,7 +506,7 @@ class Face(Shape):
@classmethod
def makePlane(cls,length,width,basePnt=None,dir=None):
- return Face(Part.makePlan(length,width,toVector(basePnt),toVector(dir)))
+ return Face(FreeCAD.Part.makePlan(length,width,toVector(basePnt),toVector(dir)))
@classmethod
def makeRuledSurface(cls,edgeOrWire1,edgeOrWire2,dist=None):
@@ -510,7 +515,7 @@ class Face(Shape):
Create a ruled surface out of two edges or wires. If wires are used then
these must have the same
"""
- return Shape.cast(Part.makeRuledSurface(edgeOrWire1.obj,edgeOrWire2.obj,dist))
+ return Shape.cast(FreeCAD.Part.makeRuledSurface(edgeOrWire1.obj,edgeOrWire2.obj,dist))
def cut(self,faceToCut):
"Remove a face from another one"
@@ -536,7 +541,7 @@ class Shell(Shape):
@classmethod
def makeShell(cls,listOfFaces):
- return Shell(Part.makeShell([i.obj for i in listOfFaces]))
+ return Shell(FreeCAD.Part.makeShell([i.obj for i in listOfFaces]))
class Solid(Shape):
@@ -563,7 +568,7 @@ class Solid(Shape):
makeBox(length,width,height,[pnt,dir]) -- Make a box located\nin pnt with the d
imensions (length,width,height)\nBy default pnt=Vector(0,0,0) and dir=Vector(0,0,1)'
"""
- return Shape.cast(Part.makeBox(length,width,height,pnt.wrapped,dir.wrapped))
+ return Shape.cast(FreeCAD.Part.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):
@@ -572,7 +577,7 @@ class Solid(Shape):
Make a cone with given radii and height\nBy default pnt=Vector(0,0,0),
dir=Vector(0,0,1) and angle=360'
"""
- return Shape.cast(Part.makeCone(radius1,radius2,height,pnt.wrapped,dir.wrapped,angleDegrees))
+ return Shape.cast(FreeCAD.Part.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):
@@ -581,7 +586,7 @@ class Solid(Shape):
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(Part.makeCylinder(radius,height,pnt.wrapped,dir.wrapped,angleDegrees))
+ return Shape.cast(FreeCAD.Part.makeCylinder(radius,height,pnt.wrapped,dir.wrapped,angleDegrees))
@classmethod
def makeTorus(cls,radius1,radius2,pnt=None,dir=None,angleDegrees1=None,angleDegrees2=None):
@@ -591,7 +596,7 @@ class Solid(Shape):
By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0
,angle1=360 and angle=360'
"""
- return Shape.cast(Part.makeTorus(radius1,radius2,pnt,dir,angleDegrees1,angleDegrees2))
+ return Shape.cast(FreeCAD.Part.makeTorus(radius1,radius2,pnt,dir,angleDegrees1,angleDegrees2))
@classmethod
def sweep(cls,profileWire,pathWire):
@@ -610,11 +615,11 @@ class Solid(Shape):
"""
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 part.
+ wants to make an infinitely thin shell for a real FreeCAD.Part.
"""
#the True flag requests building a solid instead of a shell.
- return Shape.cast(Part.makeLoft([i.wrapped for i in listOfWire],True))
+ return Shape.cast(FreeCAD.Part.makeLoft([i.wrapped for i in listOfWire],True))
@classmethod
def makeWedge(cls,xmin,ymin,zmin,z2min,x2min,xmax,ymax,zmax,z2max,x2max,pnt=None,dir=None):
@@ -624,7 +629,7 @@ class Solid(Shape):
Make a wedge located in pnt\nBy default pnt=Vector(0,0,0) and dir=Vec
tor(0,0,1)'
"""
- return Shape.cast(Part.makeWedge(xmin,ymin,zmin,z2min,x2min,xmax,ymax,zmax,z2max,x2max,pnt,dir))
+ return Shape.cast(FreeCAD.Part.makeWedge(xmin,ymin,zmin,z2min,x2min,xmax,ymax,zmax,z2max,x2max,pnt,dir))
@classmethod
def makeSphere(cls,radius,pnt=None,angleDegrees1=None,angleDegrees2=None,angleDegrees3=None):
@@ -633,7 +638,7 @@ class Solid(Shape):
Make a sphere with a giv
en radius\nBy default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360'
"""
- return Solid(Part.makeSphere(radius,pnt,angleDegrees1,angleDegrees2,angleDegrees3))
+ return Solid(FreeCAD.Part.makeSphere(radius,pnt,angleDegrees1,angleDegrees2,angleDegrees3))
@classmethod
def extrudeLinearWithRotation(cls,outerWire,innerWires,vecCenter, vecNormal,angleDegrees):
@@ -674,12 +679,12 @@ class Solid(Shape):
#make a ruled surface for each set of wires
sides = []
for w1,w2 in zip(startWires,endWires):
- rs = Part.makeRuledSurface(w1,w2)
+ rs = FreeCAD.Part.makeRuledSurface(w1,w2)
sides.append(rs)
#make faces for the top and bottom
- startFace = Part.Face(startWires)
- endFace = Part.Face(endWires)
+ startFace = FreeCAD.Part.Face(startWires)
+ endFace = FreeCAD.Part.Face(endWires)
#collect all the faces from the sides
faceList = [ startFace]
@@ -687,8 +692,8 @@ class Solid(Shape):
faceList.extend(s.Faces)
faceList.append(endFace)
- shell = Part.makeShell(faceList)
- solid = Part.makeSolid(shell)
+ shell = FreeCAD.Part.makeShell(faceList)
+ solid = FreeCAD.Part.makeSolid(shell)
return Shape.cast(solid)
@classmethod
@@ -725,7 +730,7 @@ class Solid(Shape):
for w in innerWires:
freeCADWires.append(w.wrapped)
- f = Part.Face(freeCADWires)
+ f = FreeCAD.Part.Face(freeCADWires)
result = f.extrude(vecNormal.wrapped)
return Shape.cast(result)
@@ -789,7 +794,7 @@ class Compound(Shape):
Create a compound out of a list of shapes
"""
solids = [s.wrapped for s in listOfShapes]
- c = Part.Compound(solids)
+ c = FreeCAD.Part.Compound(solids)
return Shape.cast( c)
def fuse(self,toJoin):
diff --git a/cadquery/selectors.py b/cadquery/selectors.py
index 7e85fde..6df0805 100644
--- a/cadquery/selectors.py
+++ b/cadquery/selectors.py
@@ -19,6 +19,7 @@
import re
import math
+from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound
class Selector(object):
"""
diff --git a/cadquery/workplane.py b/cadquery/workplane.py
index 1a946d1..c2e5dea 100644
--- a/cadquery/workplane.py
+++ b/cadquery/workplane.py
@@ -18,8 +18,8 @@
"""
import math
-from . import CQ
-from cadquery import Vector
+
+from cadquery import Vector,CQ,CQContext,Plane,Wire
class Workplane(CQ):
"""
diff --git a/runtests.py b/runtests.py
new file mode 100644
index 0000000..f358a5c
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,16 @@
+import sys
+from tests import *
+import cadquery
+import unittest
+
+#if you are on python 2.7, you can use -m uniitest discover.
+#but this is required for python 2.6.6 on windows. FreeCAD0.12 will not load
+#on py 2.7.x on win
+suite = unittest.TestSuite()
+
+suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCadObjects))
+suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes))
+suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors))
+suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery))
+
+unittest.TextTestRunner().run(suite)
\ No newline at end of file
diff --git a/tests/README.txt b/tests/README.txt
new file mode 100644
index 0000000..99c6a4f
--- /dev/null
+++ b/tests/README.txt
@@ -0,0 +1 @@
+It is OK for tests to import implementations like FreeCAD directly.
\ No newline at end of file
diff --git a/tests/TestCQSelectors.py b/tests/TestCQSelectors.py
index 02b3b10..fc87664 100644
--- a/tests/TestCQSelectors.py
+++ b/tests/TestCQSelectors.py
@@ -13,8 +13,9 @@ import unittest,sys
import os.path
#my modules
-from TestBase import *
+from tests import BaseTest,makeUnitCube,makeUnitSquareWire
from cadquery import *
+from cadquery import selectors
class TestCQSelectors(BaseTest):
@@ -122,8 +123,8 @@ class TestCQSelectors(BaseTest):
#faces parallel to Z axis
self.assertEqual(2, c.faces("|Z").size())
#TODO: provide short names for ParallelDirSelector
- self.assertEqual(2, c.faces(ParallelDirSelector(Vector((0,0,1)))).size()) #same thing as above
- self.assertEqual(2, c.faces(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
+ 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())
@@ -164,17 +165,17 @@ class TestCQSelectors(BaseTest):
#nearest vertex to origin is (0,0,0)
t = Vector(0.1,0.1,0.1)
- v = c.vertices(NearestToPointSelector(t)).vals()[0]
+ v = c.vertices(selectors.NearestToPointSelector(t)).vals()[0]
self.assertTupleAlmostEquals((0.0,0.0,0.0),(v.X,v.Y,v.Z),3)
t = Vector(0.1,0.1,0.2)
#nearest edge is the vertical side edge, 0,0,0 -> 0,0,1
- e = c.edges(NearestToPointSelector(t)).vals()[0]
- v = c.edges(NearestToPointSelector(t)).vertices().vals()
+ 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(NearestToPointSelector(t)).vals()
+ s = c.solids(selectors.NearestToPointSelector(t)).vals()
self.assertEqual(1,len(s))
def testFaceCount(self):
diff --git a/tests/TestCadObjects.py b/tests/TestCadObjects.py
index 7d95531..d1c9d86 100644
--- a/tests/TestCadObjects.py
+++ b/tests/TestCadObjects.py
@@ -1,11 +1,11 @@
#system modules
import sys
-#my modules
-
-import from cadquery import *
-
-
+import unittest
+from tests import BaseTest
+import FreeCAD
+from cadquery import *
+
class TestCadObjects(BaseTest):
def testVectorConstructors(self):
@@ -56,4 +56,7 @@ class TestCadObjects(BaseTest):
def testVertices(self):
e = Shape.cast(FreeCAD.Part.makeLine((0,0,0),(1,1,0)))
- self.assertEquals(2,len(e.Vertices()))
\ No newline at end of file
+ self.assertEquals(2,len(e.Vertices()))
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py
index 4c11d74..6407bd3 100644
--- a/tests/TestCadQuery.py
+++ b/tests/TestCadQuery.py
@@ -7,6 +7,8 @@ import math,sys,os.path,time
#my modules
from cadquery import *
+from cadquery import exporters
+from tests import BaseTest,writeStringToFile,makeUnitCube,readFileAsString,makeUnitSquareWire,makeCube
#where unit test output will be saved
OUTDIR = "c:/temp"
@@ -739,16 +741,6 @@ class TestCadQuery(BaseTest):
self.saveModel(s3)
- def testTwistedGear3(self):
- pts = plugins.make_gear(14.5,10,2.5) #make involutes
- s = Workplane("XY").polyline(pts).twistExtrude(4.0,8.0)
- #s2 = s.faces(">Z").workplane().transformed(rotate=Vector(0,0,8)).polyline(pts).twistExtrude(4.0,-8.0,combine=False)
- #s3 = s.union(s2)
- #s.val().exportStl("c:\\temp\\pleasework3.stl")
- #s3.val().exportStl("c:\\temp\\pleasework5.stl")
- self.saveModel(s)
-
-
def testEnclosure(self):
"""
Builds an electronics enclosure
diff --git a/tests/TestSVGexporter.py b/tests/TestSVGexporter.py
index 6481911..4c32874 100644
--- a/tests/TestSVGexporter.py
+++ b/tests/TestSVGexporter.py
@@ -3,7 +3,7 @@ __author__ = 'dcowden'
from cadquery import *
import unittest,sys
-import MakeTestObjects
+from tests import MakeTestObjects
import SVGexporter
class TestCadQuery(unittest.TestCase):
diff --git a/tests/TestWorkplanes.py b/tests/TestWorkplanes.py
index 28c2b59..54f908e 100644
--- a/tests/TestWorkplanes.py
+++ b/tests/TestWorkplanes.py
@@ -5,9 +5,9 @@
#my modules
from cadquery import *
+from tests import BaseTest,toTuple
-class TestPlane(BaseTest):
-
+class TestWorkplanes(BaseTest):
def testYZPlaneOrigins(self):
#xy plane-- with origin at x=0.25
diff --git a/tests/__init__.py b/tests/__init__.py
index cebcee0..faff2a3 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -32,7 +32,7 @@ def makeCube(size):
def toTuple(v):
"convert a vector or a vertex to a 3-tuple: x,y,z"
pnt = v
- if type(v) == Base.Vector:
+ if type(v) == FreeCAD.Base.Vector:
return (v.Point.x,v.Point.y,v.Point.z)
elif type(v) == Vector:
return v.toTuple()
@@ -45,3 +45,5 @@ class BaseTest(unittest.TestCase):
def assertTupleAlmostEquals(self,expected,actual,places):
for i,j in zip(actual,expected):
self.assertAlmostEquals(i,j,places)
+
+__all__ = [ 'TestCadObjects','TestCadQuery','TestCQSelectors','TestWorkplanes']