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']