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