From 7384d9b046ea812d39430ec251657a1ba6cbffe2 Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Sat, 12 Dec 2015 15:33:19 -0500 Subject: [PATCH] fixed setup.py issue --- build/lib.linux-i686-2.7/cadquery/__init__.py | 21 + .../cadquery/contrib/__init__.py | 18 + build/lib.linux-i686-2.7/cadquery/cq.py | 2467 +++++++++++++++++ .../cadquery/cq_directive.py | 85 + build/lib.linux-i686-2.7/cadquery/cqgi.py | 425 +++ .../cadquery/freecad_impl/__init__.py | 112 + .../cadquery/freecad_impl/exporters.py | 392 +++ .../cadquery/freecad_impl/geom.py | 647 +++++ .../cadquery/freecad_impl/importers.py | 71 + .../cadquery/freecad_impl/shapes.py | 982 +++++++ .../cadquery/plugins/__init__.py | 18 + .../lib.linux-i686-2.7/cadquery/selectors.py | 474 ++++ build/lib.linux-i686-2.7/tests/TestCQGI.py | 170 ++ .../tests/TestCQSelectors.py | 358 +++ .../tests/TestCadObjects.py | 86 + .../lib.linux-i686-2.7/tests/TestCadQuery.py | 1298 +++++++++ .../lib.linux-i686-2.7/tests/TestExporters.py | 43 + .../lib.linux-i686-2.7/tests/TestImporters.py | 54 + .../tests/TestWorkplanes.py | 125 + build/lib.linux-i686-2.7/tests/__init__.py | 54 + cadquery.egg-info/PKG-INFO | 48 +- cadquery.egg-info/SOURCES.txt | 4 +- setup.py | 2 +- 23 files changed, 7934 insertions(+), 20 deletions(-) create mode 100644 build/lib.linux-i686-2.7/cadquery/__init__.py create mode 100644 build/lib.linux-i686-2.7/cadquery/contrib/__init__.py create mode 100644 build/lib.linux-i686-2.7/cadquery/cq.py create mode 100644 build/lib.linux-i686-2.7/cadquery/cq_directive.py create mode 100644 build/lib.linux-i686-2.7/cadquery/cqgi.py create mode 100644 build/lib.linux-i686-2.7/cadquery/freecad_impl/__init__.py create mode 100644 build/lib.linux-i686-2.7/cadquery/freecad_impl/exporters.py create mode 100644 build/lib.linux-i686-2.7/cadquery/freecad_impl/geom.py create mode 100644 build/lib.linux-i686-2.7/cadquery/freecad_impl/importers.py create mode 100644 build/lib.linux-i686-2.7/cadquery/freecad_impl/shapes.py create mode 100644 build/lib.linux-i686-2.7/cadquery/plugins/__init__.py create mode 100644 build/lib.linux-i686-2.7/cadquery/selectors.py create mode 100644 build/lib.linux-i686-2.7/tests/TestCQGI.py create mode 100644 build/lib.linux-i686-2.7/tests/TestCQSelectors.py create mode 100644 build/lib.linux-i686-2.7/tests/TestCadObjects.py create mode 100644 build/lib.linux-i686-2.7/tests/TestCadQuery.py create mode 100644 build/lib.linux-i686-2.7/tests/TestExporters.py create mode 100644 build/lib.linux-i686-2.7/tests/TestImporters.py create mode 100644 build/lib.linux-i686-2.7/tests/TestWorkplanes.py create mode 100644 build/lib.linux-i686-2.7/tests/__init__.py diff --git a/build/lib.linux-i686-2.7/cadquery/__init__.py b/build/lib.linux-i686-2.7/cadquery/__init__.py new file mode 100644 index 0000000..05f7120 --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/__init__.py @@ -0,0 +1,21 @@ +#these items point to the freecad implementation +from .freecad_impl.geom import Plane,BoundBox,Vector,Matrix,sortWiresByBuildOrder +from .freecad_impl.shapes import Shape,Vertex,Edge,Face,Wire,Solid,Shell,Compound +from .freecad_impl import exporters +from .freecad_impl import importers + +#these items are the common implementation + +#the order of these matter +from .selectors import * +from .cq import * + + +__all__ = [ + 'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder', + 'Shape','Vertex','Edge','Wire','Solid','Shell','Compound','exporters', 'importers', + 'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector', + 'TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins' +] + +__version__ = "0.3.0" diff --git a/build/lib.linux-i686-2.7/cadquery/contrib/__init__.py b/build/lib.linux-i686-2.7/cadquery/contrib/__init__.py new file mode 100644 index 0000000..67c7b68 --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/contrib/__init__.py @@ -0,0 +1,18 @@ +""" + Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC + + This file is part of CadQuery. + + CadQuery is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + CadQuery is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see +""" diff --git a/build/lib.linux-i686-2.7/cadquery/cq.py b/build/lib.linux-i686-2.7/cadquery/cq.py new file mode 100644 index 0000000..59a1180 --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/cq.py @@ -0,0 +1,2467 @@ +""" + Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC + + This file is part of CadQuery. + + CadQuery is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + CadQuery is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see +""" + +import time +import math +from cadquery import * +from cadquery import selectors +from cadquery import exporters + + +class CQContext(object): + """ + A shared context for modeling. + + All objects in the same CQ chain share a reference to this same object instance + which allows for shared state when needed, + """ + def __init__(self): + self.pendingWires = [] # a list of wires that have been created and need to be extruded + self.pendingEdges = [] # a list of created pending edges that need to be joined into wires + # a reference to the first point for a set of edges. + # Used to determine how to behave when close() is called + self.firstPoint = None + self.tolerance = 0.0001 # user specified tolerance + + +class CQ(object): + """ + Provides enhanced functionality for a wrapped CAD primitive. + + Examples include feature selection, feature creation, 2d drawing + using work planes, and 3d operations like fillets, shells, and splitting + """ + + def __init__(self, obj): + """ + Construct a new CadQuery (CQ) object that wraps a CAD primitive. + + :param obj: Object to Wrap. + :type obj: A CAD Primitive ( wire,vertex,face,solid,edge ) + """ + self.objects = [] + self.ctx = CQContext() + self.parent = None + + if obj: # guarded because sometimes None for internal use + self.objects.append(obj) + + def newObject(self, objlist): + """ + Make a new CQ object. + + :param objlist: The stack of objects to use + :type objlist: a list of CAD primitives ( wire,face,edge,solid,vertex,etc ) + + The parent of the new object will be set to the current object, + to preserve the chain correctly. + + Custom plugins and subclasses should use this method to create new CQ objects + correctly. + """ + r = CQ(None) # create a completely blank one + r.parent = self + r.ctx = self.ctx # context solid remains the same + r.objects = list(objlist) + return r + + def _collectProperty(self, propName): + """ + Collects all of the values for propName, + for all items on the stack. + FreeCAD objects do not implement id correctly, + so hashCode is used to ensure we don't add the same + object multiple times. + + One weird use case is that the stack could have a solid reference object + on it. This is meant to be a reference to the most recently modified version + of the context solid, whatever it is. + """ + all = {} + for o in self.objects: + + # tricky-- if an object is a compound of solids, + # do not return all of the solids underneath-- typically + # then we'll keep joining to ourself + if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() == 'Compound': + for i in getattr(o, 'Compounds')(): + all[i.hashCode()] = i + else: + if hasattr(o, propName): + for i in getattr(o, propName)(): + all[i.hashCode()] = i + + return list(all.values()) + + def split(self, keepTop=False, keepBottom=False): + """ + Splits a solid on the stack into two parts, optionally keeping the separate parts. + + :param boolean keepTop: True to keep the top, False or None to discard it + :param boolean keepBottom: True to keep the bottom, False or None to discard it + :raises: ValueError if keepTop and keepBottom are both false. + :raises: ValueError if there is not a solid in the current stack or the parent chain + :returns: CQ object with the desired objects on the stack. + + The most common operation splits a solid and keeps one half. This sample creates + split bushing:: + + #drill a hole in the side + c = Workplane().box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()F + #now cut it in half sideways + c.faces(">Y").workplane(-0.5).split(keepTop=True) + """ + + solid = self.findSolid() + + if (not keepTop) and (not keepBottom): + raise ValueError("You have to keep at least one half") + + maxDim = solid.BoundingBox().DiagonalLength * 10.0 + topCutBox = self.rect(maxDim, maxDim)._extrude(maxDim) + bottomCutBox = self.rect(maxDim, maxDim)._extrude(-maxDim) + + top = solid.cut(bottomCutBox) + bottom = solid.cut(topCutBox) + + if keepTop and keepBottom: + # Put both on the stack, leave original unchanged. + return self.newObject([top, bottom]) + else: + # Put the one we are keeping on the stack, and also update the + # context solidto the one we kept. + if keepTop: + solid.wrapped = top.wrapped + return self.newObject([top]) + else: + solid.wrapped = bottom.wrapped + return self.newObject([bottom]) + + def combineSolids(self, otherCQToCombine=None): + """ + !!!DEPRECATED!!! use union() + Combines all solids on the current stack, and any context object, together + into a single object. + + After the operation, the returned solid is also the context solid. + + :param otherCQToCombine: another CadQuery to combine. + :return: a cQ object with the resulting combined solid on the stack. + + Most of the time, both objects will contain a single solid, which is + combined and returned on the stack of the new object. + """ + #loop through current stack objects, and combine them + #TODO: combine other types of objects as well, like edges and wires + toCombine = self.solids().vals() + + if otherCQToCombine: + for obj in otherCQToCombine.solids().vals(): + toCombine.append(obj) + + if len(toCombine) < 1: + raise ValueError("Cannot Combine: at least one solid required!") + + #get context solid and we don't want to find our own objects + ctxSolid = self.findSolid(searchStack=False, searchParents=True) + + if ctxSolid is None: + ctxSolid = toCombine.pop(0) + + #now combine them all. make sure to save a reference to the ctxSolid pointer! + s = ctxSolid + for tc in toCombine: + s = s.fuse(tc) + + ctxSolid.wrapped = s.wrapped + return self.newObject([s]) + + def all(self): + """ + Return a list of all CQ objects on the stack. + + useful when you need to operate on the elements + individually. + + Contrast with vals, which returns the underlying + objects for all of the items on the stack + """ + return [self.newObject([o]) for o in self.objects] + + def size(self): + """ + Return the number of objects currently on the stack + """ + return len(self.objects) + + def vals(self): + """ + get the values in the current list + + :rtype: list of FreeCAD objects + :returns: the values of the objects on the stack. + + Contrast with :py:meth:`all`, which returns CQ objects for all of the items on the stack + """ + return self.objects + + def add(self, obj): + """ + Adds an object or a list of objects to the stack + + :param obj: an object to add + :type obj: a CQ object, CAD primitive, or list of CAD primitives + :return: a CQ object with the requested operation performed + + If an CQ object, the values of that object's stack are added. If a list of cad primitives, + they are all added. If a single CAD primitive it is added + + Used in rare cases when you need to combine the results of several CQ results + into a single CQ object. Shelling is one common example + """ + if type(obj) == list: + self.objects.extend(obj) + elif type(obj) == CQ or type(obj) == Workplane: + self.objects.extend(obj.objects) + else: + self.objects.append(obj) + return self + + def val(self): + """ + Return the first value on the stack + + :return: the first value on the stack. + :rtype: A FreeCAD object or a SolidReference + """ + return self.objects[0] + + def toFreecad(self): + """ + Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code + needed when rendering a model in FreeCAD's 3D view. + :return: The wrapped FreeCAD object + :rtype A FreeCAD object or a SolidReference + """ + + return self.objects[0].wrapped + + def workplane(self, offset=0.0, invert=False, centerOption='CenterOfMass'): + """ + Creates a new 2-D workplane, located relative to the first face on the stack. + + :param offset: offset for the work plane in the Z direction. Default + :param invert: invert the Z direction from that of the face. + :type offset: float or None=0.0 + :type invert: boolean or None=False + :rtype: Workplane object ( which is a subclass of CQ ) + + The first element on the stack must be a face, a set of + co-planar faces or a vertex. If a vertex, then the parent + item on the chain immediately before the vertex must be a + face. + + The result will be a 2-d working plane + with a new coordinate system set up as follows: + + * The origin will be located in the *center* of the + face/faces, if a face/faces was selected. If a vertex was + selected, the origin will be at the vertex, and located + on the face. + * The Z direction will be normal to the plane of the face,computed + at the center point. + * The X direction will be parallel to the x-y plane. If the workplane is parallel to + the global x-y plane, the x direction of the workplane will co-incide with the + global x direction. + + Most commonly, the selected face will be planar, and the workplane lies in the same plane + of the face ( IE, offset=0). Occasionally, it is useful to define a face offset from + an existing surface, and even more rarely to define a workplane based on a face that is + not planar. + + To create a workplane without first having a face, use the Workplane() method. + + Future Enhancements: + * Allow creating workplane from planar wires + * Allow creating workplane based on an arbitrary point on a face, not just the center. + For now you can work around by creating a workplane and then offsetting the center + afterwards. + """ + def _isCoPlanar(f0, f1): + """Test if two faces are on the same plane.""" + p0 = f0.Center() + p1 = f1.Center() + n0 = f0.normalAt() + n1 = f1.normalAt() + + # test normals (direction of planes) + if not ((abs(n0.x-n1.x) < self.ctx.tolerance) or + (abs(n0.y-n1.y) < self.ctx.tolerance) or + (abs(n0.z-n1.z) < self.ctx.tolerance)): + return False + + # test if p1 is on the plane of f0 (offset of planes) + return abs(n0.dot(p0.sub(p1)) < self.ctx.tolerance) + + def _computeXdir(normal): + """ + Figures out the X direction based on the given normal. + :param :normal The direction that's normal to the plane. + :type :normal A Vector + :return A vector representing the X direction. + """ + xd = Vector(0, 0, 1).cross(normal) + if xd.Length < self.ctx.tolerance: + #this face is parallel with the x-y plane, so choose x to be in global coordinates + xd = Vector(1, 0, 0) + return xd + + if len(self.objects) > 1: + # are all objects 'PLANE'? + if not all(o.geomType() == 'PLANE' for o in self.objects): + raise ValueError("If multiple objects selected, they all must be planar faces.") + + # are all faces co-planar with each other? + if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]): + raise ValueError("Selected faces must be co-planar.") + + if centerOption == 'CenterOfMass': + center = Shape.CombinedCenter(self.objects) + elif centerOption == 'CenterOfBoundBox': + center = Shape.CombinedCenterOfBoundBox(self.objects) + + normal = self.objects[0].normalAt() + xDir = _computeXdir(normal) + + else: + obj = self.objects[0] + + if isinstance(obj, Face): + if centerOption == 'CenterOfMass': + center = obj.Center() + elif centerOption == 'CenterOfBoundBox': + center = obj.CenterOfBoundBox() + normal = obj.normalAt(center) + xDir = _computeXdir(normal) + else: + if hasattr(obj, 'Center'): + if centerOption == 'CenterOfMass': + center = obj.Center() + elif centerOption == 'CenterOfBoundBox': + center = obj.CenterOfBoundBox() + normal = self.plane.zDir + xDir = self.plane.xDir + else: + raise ValueError("Needs a face or a vertex or point on a work plane") + + #invert if requested + if invert: + normal = normal.multiply(-1.0) + + #offset origin if desired + offsetVector = normal.normalized().multiply(offset) + offsetCenter = center.add(offsetVector) + + #make the new workplane + plane = Plane(offsetCenter, xDir, normal) + s = Workplane(plane) + s.parent = self + s.ctx = self.ctx + + #a new workplane has the center of the workplane on the stack + return s + + def first(self): + """ + Return the first item on the stack + :returns: the first item on the stack. + :rtype: a CQ object + """ + return self.newObject(self.objects[0:1]) + + def item(self, i): + """ + + Return the ith item on the stack. + :rtype: a CQ object + """ + return self.newObject([self.objects[i]]) + + def last(self): + """ + Return the last item on the stack. + :rtype: a CQ object + """ + return self.newObject([self.objects[-1]]) + + def end(self): + """ + Return the parent of this CQ element + :rtype: a CQ object + :raises: ValueError if there are no more parents in the chain. + + For example:: + + CQ(obj).faces("+Z").vertices().end() + + will return the same as:: + + CQ(obj).faces("+Z") + """ + if self.parent: + return self.parent + else: + raise ValueError("Cannot End the chain-- no parents!") + + def findSolid(self, searchStack=True, searchParents=True): + """ + Finds the first solid object in the chain, searching from the current node + backwards through parents until one is found. + + :param searchStack: should objects on the stack be searched first. + :param searchParents: should parents be searched? + :raises: ValueError if no solid is found in the current object or its parents, + and errorOnEmpty is True + + This function is very important for chains that are modifying a single parent object, + most often a solid. + + Most of the time, a chain defines or selects a solid, and then modifies it using workplanes + or other operations. + + Plugin Developers should make use of this method to find the solid that should be modified, + if the plugin implements a unary operation, or if the operation will automatically merge its + results with an object already on the stack. + """ + #notfound = ValueError("Cannot find a Valid Solid to Operate on!") + + if searchStack: + for s in self.objects: + if isinstance(s, Solid): + return s + elif isinstance(s, Compound): + return s.Solids() + + if searchParents and self.parent is not None: + return self.parent.findSolid(searchStack=True, searchParents=searchParents) + + return None + + def _selectObjects(self, objType, selector=None): + """ + Filters objects of the selected type with the specified selector,and returns results + + :param objType: the type of object we are searching for + :type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid) + :return: a CQ object with the selected objects on the stack. + + **Implementation Note**: This is the base implementation of the vertices,edges,faces, + solids,shells, and other similar selector methods. It is a useful extension point for + plugin developers to make other selector methods. + """ + # A single list of all faces from all objects on the stack + toReturn = self._collectProperty(objType) + + if selector is not None: + if isinstance(selector, str) or isinstance(selector, unicode): + selectorObj = selectors.StringSyntaxSelector(selector) + else: + selectorObj = selector + toReturn = selectorObj.filter(toReturn) + + return self.newObject(toReturn) + + def vertices(self, selector=None): + """ + Select the vertices of objects on the stack, optionally filtering the selection. If there + are multiple objects on the stack, the vertices of all objects are collected and a list of + all the distinct vertices is returned. + + :param selector: + :type selector: None, a Selector object, or a string selector expression. + :return: a CQ object who's stack contains the *distinct* vertices of *all* objects on the + current stack, after being filtered by the selector, if provided + + If there are no vertices for any objects on the current stack, an empty CQ object + is returned + + The typical use is to select the vertices of a single object on the stack. For example:: + + Workplane().box(1,1,1).faces("+Z").vertices().size() + + returns 4, because the topmost face of cube will contain four vertices. While this:: + + Workplane().box(1,1,1).faces().vertices().size() + + returns 8, because a cube has a total of 8 vertices + + **Note** Circles are peculiar, they have a single vertex at the center! + + :py:class:`StringSyntaxSelector` + + """ + return self._selectObjects('Vertices', selector) + + def faces(self, selector=None): + """ + Select the faces of objects on the stack, optionally filtering the selection. If there are + multiple objects on the stack, the faces of all objects are collected and a list of all the + distinct faces is returned. + + :param selector: A selector + :type selector: None, a Selector object, or a string selector expression. + :return: a CQ object who's stack contains all of the *distinct* faces of *all* objects on + the current stack, filtered by the provided selector. + + If there are no vertices for any objects on the current stack, an empty CQ object + is returned. + + The typical use is to select the faces of a single object on the stack. For example:: + + CQ(aCube).faces("+Z").size() + + returns 1, because a cube has one face with a normal in the +Z direction. Similarly:: + + CQ(aCube).faces().size() + + returns 6, because a cube has a total of 6 faces, And:: + + CQ(aCube).faces("|Z").size() + + returns 2, because a cube has 2 faces having normals parallel to the z direction + + See more about selectors HERE + """ + return self._selectObjects('Faces', selector) + + def edges(self, selector=None): + """ + Select the edges of objects on the stack, optionally filtering the selection. If there are + multiple objects on the stack, the edges of all objects are collected and a list of all the + distinct edges is returned. + + :param selector: A selector + :type selector: None, a Selector object, or a string selector expression. + :return: a CQ object who's stack contains all of the *distinct* edges of *all* objects on + the current stack, filtered by the provided selector. + + If there are no edges for any objects on the current stack, an empty CQ object is returned + + The typical use is to select the edges of a single object on the stack. For example:: + + CQ(aCube).faces("+Z").edges().size() + + returns 4, because a cube has one face with a normal in the +Z direction. Similarly:: + + CQ(aCube).edges().size() + + returns 12, because a cube has a total of 12 edges, And:: + + CQ(aCube).edges("|Z").size() + + returns 4, because a cube has 4 edges parallel to the z direction + + See more about selectors HERE + """ + return self._selectObjects('Edges', selector) + + def wires(self, selector=None): + """ + Select the wires of objects on the stack, optionally filtering the selection. If there are + multiple objects on the stack, the wires of all objects are collected and a list of all the + distinct wires is returned. + + :param selector: A selector + :type selector: None, a Selector object, or a string selector expression. + :return: a CQ object who's stack contains all of the *distinct* wires of *all* objects on + the current stack, filtered by the provided selector. + + If there are no wires for any objects on the current stack, an empty CQ object is returned + + The typical use is to select the wires of a single object on the stack. For example:: + + CQ(aCube).faces("+Z").wires().size() + + returns 1, because a face typically only has one outer wire + + See more about selectors HERE + """ + return self._selectObjects('Wires', selector) + + def solids(self, selector=None): + """ + Select the solids of objects on the stack, optionally filtering the selection. If there are + multiple objects on the stack, the solids of all objects are collected and a list of all the + distinct solids is returned. + + :param selector: A selector + :type selector: None, a Selector object, or a string selector expression. + :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on + the current stack, filtered by the provided selector. + + If there are no solids for any objects on the current stack, an empty CQ object is returned + + The typical use is to select the a single object on the stack. For example:: + + CQ(aCube).solids().size() + + returns 1, because a cube consists of one solid. + + It is possible for single CQ object ( or even a single CAD primitive ) to contain + multiple solids. + + See more about selectors HERE + """ + return self._selectObjects('Solids', selector) + + def shells(self, selector=None): + """ + Select the shells of objects on the stack, optionally filtering the selection. If there are + multiple objects on the stack, the shells of all objects are collected and a list of all the + distinct shells is returned. + + :param selector: A selector + :type selector: None, a Selector object, or a string selector expression. + :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on + the current stack, filtered by the provided selector. + + If there are no shells for any objects on the current stack, an empty CQ object is returned + + Most solids will have a single shell, which represents the outer surface. A shell will + typically be composed of multiple faces. + + See more about selectors HERE + """ + return self._selectObjects('Shells', selector) + + def compounds(self, selector=None): + """ + Select compounds on the stack, optionally filtering the selection. If there are multiple + objects on the stack, they are collected and a list of all the distinct compounds + is returned. + + :param selector: A selector + :type selector: None, a Selector object, or a string selector expression. + :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on + the current stack, filtered by the provided selector. + + A compound contains multiple CAD primitives that resulted from a single operation, such as + a union, cut, split, or fillet. Compounds can contain multiple edges, wires, or solids. + + See more about selectors HERE + """ + return self._selectObjects('Compounds', selector) + + def toSvg(self, opts=None): + """ + Returns svg text that represents the first item on the stack. + + for testing purposes. + + :param opts: svg formatting options + :type opts: dictionary, width and height + :return: a string that contains SVG that represents this item. + """ + return exporters.getSVG(self.val().wrapped, opts) + + def exportSvg(self, fileName): + """ + Exports the first item on the stack as an SVG file + + For testing purposes mainly. + + :param fileName: the filename to export + :type fileName: String, absolute path to the file + """ + exporters.exportSVG(self, fileName) + + def rotateAboutCenter(self, axisEndPoint, angleDegrees): + """ + Rotates all items on the stack by the specified angle, about the specified axis + + The center of rotation is a vector starting at the center of the object on the stack, + and ended at the specified point. + + :param axisEndPoint: the second point of axis of rotation + :type axisEndPoint: a three-tuple in global coordinates + :param angleDegrees: the rotation angle, in degrees + :type angleDegrees: float + :returns: a CQ object, with all items rotated. + + WARNING: This version returns the same cq object instead of a new one-- the + old object is not accessible. + + Future Enhancements: + * A version of this method that returns a transformed copy, rather than modifying + the originals + * This method doesnt expose a very good interface, because the axis of rotation + could be inconsistent between multiple objects. This is because the beginning + of the axis is variable, while the end is fixed. This is fine when operating on + one object, but is not cool for multiple. + """ + + #center point is the first point in the vector + endVec = Vector(axisEndPoint) + + def _rot(obj): + startPt = obj.Center() + endPt = startPt + endVec + return obj.rotate(startPt, endPt, angleDegrees) + + return self.each(_rot, False) + + def rotate(self, axisStartPoint, axisEndPoint, angleDegrees): + """ + Returns a copy of all of the items on the stack rotated through and angle around the axis + of rotation. + + :param axisStartPoint: The first point of the axis of rotation + :type axisStartPoint: a 3-tuple of floats + :type axisEndPoint: The second point of the axis of rotation + :type axisEndPoint: a 3-tuple of floats + :param angleDegrees: the rotation angle, in degrees + :type angleDegrees: float + :returns: a CQ object + """ + return self.newObject([o.rotate(axisStartPoint, axisEndPoint, angleDegrees) + for o in self.objects]) + + def translate(self, vec): + """ + Returns a copy of all of the items on the stack moved by the specified translation vector. + + :param tupleDistance: distance to move, in global coordinates + :type tupleDistance: a 3-tuple of float + :returns: a CQ object + """ + return self.newObject([o.translate(vec) for o in self.objects]) + + + def shell(self, thickness): + """ + Remove the selected faces to create a shell of the specified thickness. + + To shell, first create a solid, and *in the same chain* select the faces you wish to remove. + + :param thickness: a positive float, representing the thickness of the desired shell. + Negative values shell inwards, positive values shell outwards. + :raises: ValueError if the current stack contains objects that are not faces of a solid + further up in the chain. + :returns: a CQ object with the resulting shelled solid selected. + + This example will create a hollowed out unit cube, where the top most face is open, + and all other walls are 0.2 units thick:: + + Workplane().box(1,1,1).faces("+Z").shell(0.2) + + Shelling is one of the cases where you may need to use the add method to select several + faces. For example, this example creates a 3-walled corner, by removing three faces + of a cube:: + + s = Workplane().box(1,1,1) + s1 = s.faces("+Z") + s1.add(s.faces("+Y")).add(s.faces("+X")) + self.saveModel(s1.shell(0.2)) + + This fairly yucky syntax for selecting multiple faces is planned for improvement + + **Note**: When sharp edges are shelled inwards, they remain sharp corners, but **outward** + shells are automatically filleted, because an outward offset from a corner generates + a radius. + + + Future Enhancements: + Better selectors to make it easier to select multiple faces + """ + solidRef = self.findSolid() + + for f in self.objects: + if type(f) != Face: + raise ValueError("Shelling requires that faces be selected") + + s = solidRef.shell(self.objects, thickness) + solidRef.wrapped = s.wrapped + return self.newObject([s]) + + def fillet(self, radius): + """ + Fillets a solid on the selected edges. + + The edges on the stack are filleted. The solid to which the edges belong must be in the + parent chain of the selected edges. + + :param radius: the radius of the fillet, must be > zero + :type radius: positive float + :raises: ValueError if at least one edge is not selected + :raises: ValueError if the solid containing the edge is not in the chain + :returns: cq object with the resulting solid selected. + + This example will create a unit cube, with the top edges filleted:: + + s = Workplane().box(1,1,1).faces("+Z").edges().fillet(0.1) + """ + # TODO: we will need much better edge selectors for this to work + # TODO: ensure that edges selected actually belong to the solid in the chain, otherwise, + # TODO: we segfault + + solid = self.findSolid() + + edgeList = self.edges().vals() + if len(edgeList) < 1: + raise ValueError("Fillets requires that edges be selected") + + s = solid.fillet(radius, edgeList) + solid.wrapped = s.wrapped + return self.newObject([s]) + + def chamfer(self, length, length2=None): + """ + Chamfers a solid on the selected edges. + + The edges on the stack are chamfered. The solid to which the + edges belong must be in the parent chain of the selected + edges. + + Optional parameter `length2` can be supplied with a different + value than `length` for a chamfer that is shorter on one side + longer on the other side. + + :param length: the length of the fillet, must be greater than zero + :param length2: optional parameter for asymmetrical chamfer + :type length: positive float + :type length2: positive float + :raises: ValueError if at least one edge is not selected + :raises: ValueError if the solid containing the edge is not in the chain + :returns: cq object with the resulting solid selected. + + This example will create a unit cube, with the top edges chamfered:: + + s = Workplane("XY").box(1,1,1).faces("+Z").chamfer(0.1) + + This example will create chamfers longer on the sides:: + + s = Workplane("XY").box(1,1,1).faces("+Z").chamfer(0.2, 0.1) + """ + solid = self.findSolid() + + edgeList = self.edges().vals() + if len(edgeList) < 1: + raise ValueError("Chamfer requires that edges be selected") + + s = solid.chamfer(length, length2, edgeList) + + solid.wrapped = s.wrapped + return self.newObject([s]) + + +class Workplane(CQ): + """ + Defines a coordinate system in space, in which 2-d coordinates can be used. + + :param plane: the plane in which the workplane will be done + :type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right) + :param origin: the desired origin of the new workplane + :type origin: a 3-tuple in global coordinates, or None to default to the origin + :param obj: an object to use initially for the stack + :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial + stack value. + :raises: ValueError if the provided plane is not a plane, a valid named workplane + :return: A Workplane object, with coordinate system matching the supplied plane. + + The most common use is:: + + s = Workplane("XY") + + After creation, the stack contains a single point, the origin of the underlying plane, + and the *current point* is on the origin. + + .. note:: + You can also create workplanes on the surface of existing faces using + :py:meth:`CQ.workplane` + """ + + FOR_CONSTRUCTION = 'ForConstruction' + + def __init__(self, inPlane, origin=(0, 0, 0), obj=None): + """ + make a workplane from a particular plane + + :param inPlane: the plane in which the workplane will be done + :type inPlane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right) + :param origin: the desired origin of the new workplane + :type origin: a 3-tuple in global coordinates, or None to default to the origin + :param obj: an object to use initially for the stack + :type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial + stack value. + :raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ + :return: A Workplane object, with coordinate system matching the supplied plane. + + The most common use is:: + + s = Workplane("XY") + + After creation, the stack contains a single point, the origin of the underlying plane, and + the *current point* is on the origin. + """ + + if inPlane.__class__.__name__ == 'Plane': + tmpPlane = inPlane + elif isinstance(inPlane, str) or isinstance(inPlane, unicode): + tmpPlane = Plane.named(inPlane, origin) + else: + tmpPlane = None + + if tmpPlane is None: + raise ValueError( + 'Provided value {} is not a valid work plane'.format(inPlane)) + + self.obj = obj + self.plane = tmpPlane + self.firstPoint = None + # Changed so that workplane has the center as the first item on the stack + self.objects = [self.plane.origin] + self.parent = None + self.ctx = CQContext() + + def transformed(self, rotate=(0, 0, 0), offset=(0, 0, 0)): + """ + Create a new workplane based on the current one. + The origin of the new plane is located at the existing origin+offset vector, where offset is + given in coordinates local to the current plane + The new plane is rotated through the angles specified by the components of the rotation + vector. + :param rotate: 3-tuple of angles to rotate, in degrees relative to work plane coordinates + :param offset: 3-tuple to offset the new plane, in local work plane coordinates + :return: a new work plane, transformed as requested + """ + + #old api accepted a vector, so we'll check for that. + if rotate.__class__.__name__ == 'Vector': + rotate = rotate.toTuple() + + if offset.__class__.__name__ == 'Vector': + offset = offset.toTuple() + + p = self.plane.rotated(rotate) + p.origin = self.plane.toWorldCoords(offset) + ns = self.newObject([p.origin]) + ns.plane = p + + return ns + + def newObject(self, objlist): + """ + Create a new workplane object from this one. + + Overrides CQ.newObject, and should be used by extensions, plugins, and + subclasses to create new objects. + + :param objlist: new objects to put on the stack + :type objlist: a list of CAD primitives + :return: a new Workplane object with the current workplane as a parent. + """ + + #copy the current state to the new object + ns = Workplane("XY") + ns.plane = self.plane + ns.parent = self + ns.objects = list(objlist) + ns.ctx = self.ctx + return ns + + def _findFromPoint(self, useLocalCoords=False): + """ + Finds the start point for an operation when an existing point + is implied. Examples include 2d operations such as lineTo, + which allows specifying the end point, and implicitly use the + end of the previous line as the starting point + + :return: a Vector representing the point to use, or none if + such a point is not available. + + :param useLocalCoords: selects whether the point is returned + in local coordinates or global coordinates. + + The algorithm is this: + * If an Edge is on the stack, its end point is used.yp + * if a vector is on the stack, it is used + + WARNING: only the last object on the stack is used. + + NOTE: + """ + obj = self.objects[-1] + + if isinstance(obj, Edge): + p = obj.endPoint() + elif isinstance(obj, Vector): + p = obj + else: + raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) + + if useLocalCoords: + return self.plane.toLocalCoords(p) + else: + return p + + def rarray(self, xSpacing, ySpacing, xCount, yCount, center=True): + """ + Creates an array of points and pushes them onto the stack. + If you want to position the array at another point, create another workplane + that is shifted to the position you would like to use as a reference + + :param xSpacing: spacing between points in the x direction ( must be > 0) + :param ySpacing: spacing between points in the y direction ( must be > 0) + :param xCount: number of points ( > 0 ) + :param yCount: number of points ( > 0 ) + :param center: if true, the array will be centered at the center of the workplane. if + false, the lower left corner will be at the center of the work plane + """ + + if xSpacing < 1 or ySpacing < 1 or xCount < 1 or yCount < 1: + raise ValueError("Spacing and count must be > 0 ") + + lpoints = [] # coordinates relative to bottom left point + for x in range(xCount): + for y in range(yCount): + lpoints.append((xSpacing * x, ySpacing * y)) + + #shift points down and left relative to origin if requested + if center: + xc = xSpacing*(xCount-1) * 0.5 + yc = ySpacing*(yCount-1) * 0.5 + cpoints = [] + for p in lpoints: + cpoints.append((p[0] - xc, p[1] - yc)) + lpoints = list(cpoints) + + return self.pushPoints(lpoints) + + def pushPoints(self, pntList): + """ + Pushes a list of points onto the stack as vertices. + The points are in the 2-d coordinate space of the workplane face + + :param pntList: a list of points to push onto the stack + :type pntList: list of 2-tuples, in *local* coordinates + :return: a new workplane with the desired points on the stack. + + A common use is to provide a list of points for a subsequent operation, such as creating + circles or holes. This example creates a cube, and then drills three holes through it, + based on three points:: + + s = Workplane().box(1,1,1).faces(">Z").workplane().\ + pushPoints([(-0.3,0.3),(0.3,0.3),(0,0)]) + body = s.circle(0.05).cutThruAll() + + Here the circle function operates on all three points, and is then extruded to create three + holes. See :py:meth:`circle` for how it works. + """ + vecs = [] + for pnt in pntList: + vec = self.plane.toWorldCoords(pnt) + vecs.append(vec) + + return self.newObject(vecs) + + def center(self, x, y): + """ + Shift local coordinates to the specified location. + + The location is specified in terms of local coordinates. + + :param float x: the new x location + :param float y: the new y location + :returns: the workplane object, with the center adjusted. + + The current point is set to the new center. + This method is useful to adjust the center point after it has been created automatically on + a face, but not where you'd like it to be. + + In this example, we adjust the workplane center to be at the corner of a cube, instead of + the center of a face, which is the default:: + + #this workplane is centered at x=0.5,y=0.5, the center of the upper face + s = Workplane().box(1,1,1).faces(">Z").workplane() + + s.center(-0.5,-0.5) # move the center to the corner + t = s.circle(0.25).extrude(0.2) + assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner + + The result is a cube with a round boss on the corner + """ + "Shift local coordinates to the specified location, according to current coordinates" + self.plane.setOrigin2d(x, y) + n = self.newObject([self.plane.origin]) + return n + + def lineTo(self, x, y, forConstruction=False): + """ + Make a line from the current point to the provided point + + :param float x: the x point, in workplane plane coordinates + :param float y: the y point, in workplane plane coordinates + :return: the Workplane object with the current point at the end of the new line + + see :py:meth:`line` if you want to use relative dimensions to make a line instead. + """ + startPoint = self._findFromPoint(False) + + endPoint = self.plane.toWorldCoords((x, y)) + + p = Edge.makeLine(startPoint, endPoint) + + if not forConstruction: + self._addPendingEdge(p) + + return self.newObject([p]) + + # line a specified incremental amount from current point + def line(self, xDist, yDist, forConstruction=False): + """ + Make a line from the current point to the provided point, using + dimensions relative to the current point + + :param float xDist: x distance from current point + :param float yDist: y distance from current point + :return: the workplane object with the current point at the end of the new line + + see :py:meth:`lineTo` if you want to use absolute coordinates to make a line instead. + """ + p = self._findFromPoint(True) # return local coordinates + return self.lineTo(p.x + xDist, yDist + p.y, forConstruction) + + def vLine(self, distance, forConstruction=False): + """ + Make a vertical line from the current point the provided distance + + :param float distance: (y) distance from current point + :return: the workplane object with the current point at the end of the new line + """ + return self.line(0, distance, forConstruction) + + def hLine(self, distance, forConstruction=False): + """ + Make a horizontal line from the current point the provided distance + + :param float distance: (x) distance from current point + :return: the Workplane object with the current point at the end of the new line + """ + return self.line(distance, 0, forConstruction) + + def vLineTo(self, yCoord, forConstruction=False): + """ + Make a vertical line from the current point to the provided y coordinate. + + Useful if it is more convenient to specify the end location rather than distance, + as in :py:meth:`vLine` + + :param float yCoord: y coordinate for the end of the line + :return: the Workplane object with the current point at the end of the new line + """ + p = self._findFromPoint(True) + return self.lineTo(p.x, yCoord, forConstruction) + + def hLineTo(self, xCoord, forConstruction=False): + """ + Make a horizontal line from the current point to the provided x coordinate. + + Useful if it is more convenient to specify the end location rather than distance, + as in :py:meth:`hLine` + + :param float xCoord: x coordinate for the end of the line + :return: the Workplane object with the current point at the end of the new line + """ + p = self._findFromPoint(True) + return self.lineTo(xCoord, p.y, forConstruction) + + #absolute move in current plane, not drawing + def moveTo(self, x=0, y=0): + """ + Move to the specified point, without drawing. + + :param x: desired x location, in local coordinates + :type x: float, or none for zero + :param y: desired y location, in local coordinates + :type y: float, or none for zero. + + Not to be confused with :py:meth:`center`, which moves the center of the entire + workplane, this method only moves the current point ( and therefore does not affect objects + already drawn ). + + See :py:meth:`move` to do the same thing but using relative dimensions + """ + newCenter = Vector(x, y, 0) + return self.newObject([self.plane.toWorldCoords(newCenter)]) + + #relative move in current plane, not drawing + def move(self, xDist=0, yDist=0): + """ + Move the specified distance from the current point, without drawing. + + :param xDist: desired x distance, in local coordinates + :type xDist: float, or none for zero + :param yDist: desired y distance, in local coordinates + :type yDist: float, or none for zero. + + Not to be confused with :py:meth:`center`, which moves the center of the entire + workplane, this method only moves the current point ( and therefore does not affect objects + already drawn ). + + See :py:meth:`moveTo` to do the same thing but using absolute coordinates + """ + p = self._findFromPoint(True) + newCenter = p + Vector(xDist, yDist, 0) + return self.newObject([self.plane.toWorldCoords(newCenter)]) + + def spline(self, listOfXYTuple, forConstruction=False): + """ + Create a spline interpolated through the provided points. + + :param listOfXYTuple: points to interpolate through + :type listOfXYTuple: list of 2-tuple + :return: a Workplane object with the current point at the end of the spline + + The spline will begin at the current point, and + end with the last point in the XY tuple list + + This example creates a block with a spline for one side:: + + s = Workplane(Plane.XY()) + sPnts = [ + (2.75,1.5), + (2.5,1.75), + (2.0,1.5), + (1.5,1.0), + (1.0,1.25), + (0.5,1.0), + (0,1.0) + ] + r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close() + r = r.extrude(0.5) + + *WARNING* It is fairly easy to create a list of points + that cannot be correctly interpreted as a spline. + + Future Enhancements: + * provide access to control points + """ + gstartPoint = self._findFromPoint(False) + gEndPoint = self.plane.toWorldCoords(listOfXYTuple[-1]) + + vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple] + allPoints = [gstartPoint] + vecs + + e = Edge.makeSpline(allPoints) + + if not forConstruction: + self._addPendingEdge(e) + + return self.newObject([e]) + + def threePointArc(self, point1, point2, forConstruction=False): + """ + Draw an arc from the current point, through point1, and ending at point2 + + :param point1: point to draw through + :type point1: 2-tuple, in workplane coordinates + :param point2: end point for the arc + :type point2: 2-tuple, in workplane coordinates + :return: a workplane with the current point at the end of the arc + + Future Enhancements: + provide a version that allows an arc using relative measures + provide a centerpoint arc + provide tangent arcs + """ + + gstartPoint = self._findFromPoint(False) + gpoint1 = self.plane.toWorldCoords(point1) + gpoint2 = self.plane.toWorldCoords(point2) + + arc = Edge.makeThreePointArc(gstartPoint, gpoint1, gpoint2) + + if not forConstruction: + self._addPendingEdge(arc) + + return self.newObject([arc]) + + def rotateAndCopy(self, matrix): + """ + Makes a copy of all edges on the stack, rotates them according to the + provided matrix, and then attempts to consolidate them into a single wire. + + :param matrix: a 4xr transformation matrix, in global coordinates + :type matrix: a FreeCAD Base.Matrix object + :return: a CadQuery object with consolidated wires, and any originals on the stack. + + The most common use case is to create a set of open edges, and then mirror them + around either the X or Y axis to complete a closed shape. + + see :py:meth:`mirrorX` and :py:meth:`mirrorY` to mirror about the global X and Y axes + see :py:meth:`mirrorX` and for an example + + Future Enhancements: + faster implementation: this one transforms 3 times to accomplish the result + """ + + #convert edges to a wire, if there are pending edges + n = self.wire(forConstruction=False) + + #attempt to consolidate wires together. + consolidated = n.consolidateWires() + + rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix) + + for w in rotatedWires: + consolidated.objects.append(w) + consolidated._addPendingWire(w) + + #attempt again to consolidate all of the wires + c = consolidated.consolidateWires() + + return c + + def mirrorY(self): + """ + Mirror entities around the y axis of the workplane plane. + + :return: a new object with any free edges consolidated into as few wires as possible. + + All free edges are collected into a wire, and then the wire is mirrored, + and finally joined into a new wire + + Typically used to make creating wires with symmetry easier. This line of code:: + + s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25) + + Produces a flat, heart shaped object + + Future Enhancements: + mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness + """ + tm = Matrix() + tm.rotateY(math.pi) + return self.rotateAndCopy(tm) + + def mirrorX(self): + """ + Mirror entities around the x axis of the workplane plane. + + :return: a new object with any free edges consolidated into as few wires as possible. + + All free edges are collected into a wire, and then the wire is mirrored, + and finally joined into a new wire + + Typically used to make creating wires with symmetry easier. + + Future Enhancements: + mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness + """ + tm = Matrix() + tm.rotateX(math.pi) + return self.rotateAndCopy(tm) + + def _addPendingEdge(self, edge): + """ + Queues an edge for later combination into a wire. + + :param edge: + :return: + """ + self.ctx.pendingEdges.append(edge) + + if self.ctx.firstPoint is None: + self.ctx.firstPoint = self.plane.toLocalCoords(edge.startPoint()) + + def _addPendingWire(self, wire): + """ + Queue a Wire for later extrusion + + Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids. + + but users do not normally care about these distinctions. Users 'think' in terms + of edges, and solids. + + CadQuery tracks edges as they are drawn, and automatically combines them into wires + when the user does an operation that needs it. + + Similarly, cadQuery tracks pending wires, and automatically combines them into faces + when necessary to make a solid. + """ + self.ctx.pendingWires.append(wire) + + def consolidateWires(self): + """ + Attempt to consolidate wires on the stack into a single. + If possible, a new object with the results are returned. + if not possible, the wires remain separated + + FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly sometimes + Additionally, it has a bug where a profile composed of two wires ( rather than one ) + also does not work properly. Together these are a real problem. + """ + wires = self.wires().vals() + if len(wires) < 2: + return self + + #TODO: this makes the assumption that either all wires could be combined, or none. + #in reality trying each combination of wires is probably not reasonable anyway + w = Wire.combine(wires) + + #ok this is a little tricky. if we consolidate wires, we have to actually + #modify the pendingWires collection to remove the original ones, and replace them + #with the consolidate done + #since we are already assuming that all wires could be consolidated, its easy, we just + #clear the pending wire list + r = self.newObject([w]) + r.ctx.pendingWires = [] + r._addPendingWire(w) + return r + + def wire(self, forConstruction=False): + """ + Returns a CQ object with all pending edges connected into a wire. + + All edges on the stack that can be combined will be combined into a single wire object, + and other objects will remain on the stack unmodified + + :param forConstruction: whether the wire should be used to make a solid, or if it is just + for reference + :type forConstruction: boolean. true if the object is only for reference + + This method is primarily of use to plugin developers making utilities for 2-d construction. + This method should be called when a user operation implies that 2-d construction is + finished, and we are ready to begin working in 3d + + SEE '2-d construction concepts' for a more detailed explanation of how CadQuery handles + edges, wires, etc + + Any non edges will still remain. + """ + + edges = self.ctx.pendingEdges + + #do not consolidate if there are no free edges + if len(edges) == 0: + return self + + self.ctx.pendingEdges = [] + + others = [] + for e in self.objects: + if type(e) != Edge: + others.append(e) + + + w = Wire.assembleEdges(edges) + if not forConstruction: + self._addPendingWire(w) + + return self.newObject(others + [w]) + + def each(self, callBackFunction, useLocalCoordinates=False): + """ + Runs the provided function on each value in the stack, and collects the return values into + a new CQ object. + + Special note: a newly created workplane always has its center point as its only stack item + + :param callBackFunction: the function to call for each item on the current stack. + :param useLocalCoordinates: should values be converted from local coordinates first? + :type useLocalCoordinates: boolean + + The callback function must accept one argument, which is the item on the stack, and return + one object, which is collected. If the function returns None, nothing is added to the stack. + The object passed into the callBackFunction is potentially transformed to local coordinates, + if useLocalCoordinates is true + + useLocalCoordinates is very useful for plugin developers. + + If false, the callback function is assumed to be working in global coordinates. Objects + created are added as-is, and objects passed into the function are sent in using global + coordinates + + If true, the calling function is assumed to be working in local coordinates. Objects are + transformed to local coordinates before they are passed into the callback method, and result + objects are transformed to global coordinates after they are returned. + + This allows plugin developers to create objects in local coordinates, without worrying + about the fact that the working plane is different than the global coordinate system. + + + TODO: wrapper object for Wire will clean up forConstruction flag everywhere + """ + results = [] + for obj in self.objects: + + if useLocalCoordinates: + #TODO: this needs to work for all types of objects, not just vectors! + r = callBackFunction(self.plane.toLocalCoords(obj)) + r = r.transformShape(self.plane.rG) + else: + r = callBackFunction(obj) + + if type(r) == Wire: + if not r.forConstruction: + self._addPendingWire(r) + + results.append(r) + + return self.newObject(results) + + def eachpoint(self, callbackFunction, useLocalCoordinates=False): + """ + Same as each(), except each item on the stack is converted into a point before it + is passed into the callback function. + + :return: CadQuery object which contains a list of vectors (points ) on its stack. + + :param useLocalCoordinates: should points be in local or global coordinates + :type useLocalCoordinates: boolean + + The resulting object has a point on the stack for each object on the original stack. + Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted + to a point by using their center of mass. + + If the stack has zero length, a single point is returned, which is the center of the current + workplane/coordinate system + """ + #convert stack to a list of points + pnts = [] + if len(self.objects) == 0: + #nothing on the stack. here, we'll assume we should operate with the + #origin as the context point + pnts.append(self.plane.origin) + else: + + for v in self.objects: + pnts.append(v.Center()) + + return self.newObject(pnts).each(callbackFunction, useLocalCoordinates) + + def rect(self, xLen, yLen, centered=True, forConstruction=False): + """ + Make a rectangle for each item on the stack. + + :param xLen: length in xDirection ( in workplane coordinates ) + :type xLen: float > 0 + :param yLen: length in yDirection ( in workplane coordinates ) + :type yLen: float > 0 + :param boolean centered: true if the rect is centered on the reference point, false if the + lower-left is on the reference point + :param forConstruction: should the new wires be reference geometry only? + :type forConstruction: true if the wires are for reference, false if they are creating part + geometry + :return: a new CQ object with the created wires on the stack + + A common use case is to use a for-construction rectangle to define the centers of a hole + pattern:: + + s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25) + + Creates 4 circles at the corners of a square centered on the origin. + + Future Enhancements: + better way to handle forConstruction + project points not in the workplane plane onto the workplane plane + """ + def makeRectangleWire(pnt): + # Here pnt is in local coordinates due to useLocalCoords=True + # (xc,yc,zc) = pnt.toTuple() + if centered: + p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0, 0)) + p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0, 0)) + p3 = pnt.add(Vector(xLen/2.0, yLen/2.0, 0)) + p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0, 0)) + else: + p1 = pnt + p2 = pnt.add(Vector(xLen, 0, 0)) + p3 = pnt.add(Vector(xLen, yLen, 0)) + p4 = pnt.add(Vector(0, yLen, 0)) + + w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction) + return w + #return Part.makePolygon([p1,p2,p3,p4,p1]) + + return self.eachpoint(makeRectangleWire, True) + + #circle from current point + def circle(self, radius, forConstruction=False): + """ + Make a circle for each item on the stack. + + :param radius: radius of the circle + :type radius: float > 0 + :param forConstruction: should the new wires be reference geometry only? + :type forConstruction: true if the wires are for reference, false if they are creating + part geometry + :return: a new CQ object with the created wires on the stack + + A common use case is to use a for-construction rectangle to define the centers of a + hole pattern:: + + s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25) + + Creates 4 circles at the corners of a square centered on the origin. Another common case is + to use successive circle() calls to create concentric circles. This works because the + center of a circle is its reference point:: + + s = Workplane().circle(2.0).circle(1.0) + + Creates two concentric circles, which when extruded will form a ring. + + Future Enhancements: + better way to handle forConstruction + project points not in the workplane plane onto the workplane plane + + """ + def makeCircleWire(obj): + cir = Wire.makeCircle(radius, obj, Vector(0, 0, 1)) + cir.forConstruction = forConstruction + return cir + + return self.eachpoint(makeCircleWire, useLocalCoordinates=True) + + def polygon(self, nSides, diameter, forConstruction=False): + """ + Creates a polygon inscribed in a circle of the specified diameter for each point on + the stack + + The first vertex is always oriented in the x direction. + + :param nSides: number of sides, must be > 3 + :param diameter: the size of the circle the polygon is inscribed into + :return: a polygon wire + """ + def _makePolygon(center): + #pnt is a vector in local coordinates + angle = 2.0 * math.pi / nSides + pnts = [] + for i in range(nSides+1): + pnts.append(center + Vector((diameter / 2.0 * math.cos(angle*i)), + (diameter / 2.0 * math.sin(angle*i)), 0)) + return Wire.makePolygon(pnts, forConstruction) + + return self.eachpoint(_makePolygon, True) + + def polyline(self, listOfXYTuple, forConstruction=False): + """ + Create a polyline from a list of points + + :param listOfXYTuple: a list of points in Workplane coordinates + :type listOfXYTuple: list of 2-tuples + :param forConstruction: whether or not the edges are used for reference + :type forConstruction: true if the edges are for reference, false if they are for creating geometry + part geometry + :return: a new CQ object with a list of edges on the stack + + *NOTE* most commonly, the resulting wire should be closed. + """ + + # Our list of new edges that will go into a new CQ object + edges = [] + + # The very first startPoint comes from our original object, but not after that + startPoint = self._findFromPoint(False) + + # Draw a line for each set of points, starting from the from-point of the original CQ object + for curTuple in listOfXYTuple: + endPoint = self.plane.toWorldCoords(curTuple) + + edges.append(Edge.makeLine(startPoint, endPoint)) + + # We need to move the start point for the next line that we draw or we get stuck at the same startPoint + startPoint = endPoint + + if not forConstruction: + self._addPendingEdge(edges[-1]) + + return self.newObject(edges) + + def close(self): + """ + End 2-d construction, and attempt to build a closed wire. + + :return: a CQ object with a completed wire on the stack, if possible. + + After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary + to convert the edges produced by these into one or more wires. + + When a set of edges is closed, cadQuery assumes it is safe to build the group of edges + into a wire. This example builds a simple triangular prism:: + + s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2) + """ + self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y) + + # Need to reset the first point after closing a wire + self.ctx.firstPoint=None + + return self.wire() + + def largestDimension(self): + """ + Finds the largest dimension in the stack. + Used internally to create thru features, this is how you can compute + how long or wide a feature must be to make sure to cut through all of the material + :return: A value representing the largest dimension of the first solid on the stack + """ + #TODO: this implementation is naive and returns the dims of the first solid... most of + #TODO: the time this works. but a stronger implementation would be to search all solids. + s = self.findSolid() + if s: + return s.BoundingBox().DiagonalLength * 5.0 + else: + return -1 + + def cutEach(self, fcn, useLocalCoords=False, clean=True): + """ + Evaluates the provided function at each point on the stack (ie, eachpoint) + and then cuts the result from the context solid. + :param fcn: a function suitable for use in the eachpoint method: ie, that accepts a vector + :param useLocalCoords: same as for :py:meth:`eachpoint` + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :return: a CQ object that contains the resulting solid + :raises: an error if there is not a context solid to cut from + """ + ctxSolid = self.findSolid() + if ctxSolid is None: + raise ValueError("Must have a solid in the chain to cut from!") + + #will contain all of the counterbores as a single compound + results = self.eachpoint(fcn, useLocalCoords).vals() + s = ctxSolid + for cb in results: + s = s.cut(cb) + + if clean: s = s.clean() + + ctxSolid.wrapped = s.wrapped + return self.newObject([s]) + + #but parameter list is different so a simple function pointer wont work + def cboreHole(self, diameter, cboreDiameter, cboreDepth, depth=None, clean=True): + """ + Makes a counterbored hole for each item on the stack. + + :param diameter: the diameter of the hole + :type diameter: float > 0 + :param cboreDiameter: the diameter of the cbore + :type cboreDiameter: float > 0 and > diameter + :param cboreDepth: depth of the counterbore + :type cboreDepth: float > 0 + :param depth: the depth of the hole + :type depth: float > 0 or None to drill thru the entire part. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + + The surface of the hole is at the current workplane plane. + + One hole is created for each item on the stack. A very common use case is to use a + construction rectangle to define the centers of a set of holes, like so:: + + s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane()\ + .rect(1.5,3.5,forConstruction=True)\ + .vertices().cboreHole(0.125, 0.25,0.125,depth=None) + + This sample creates a plate with a set of holes at the corners. + + **Plugin Note**: this is one example of the power of plugins. Counterbored holes are quite + time consuming to create, but are quite easily defined by users. + + see :py:meth:`cskHole` to make countersinks instead of counterbores + """ + if depth is None: + depth = self.largestDimension() + + def _makeCbore(center): + """ + Makes a single hole with counterbore at the supplied point + returns a solid suitable for subtraction + pnt is in local coordinates + """ + boreDir = Vector(0, 0, -1) + #first make the hole + hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coordianates! + + #add the counter bore + cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir) + r = hole.fuse(cbore) + return r + + return self.cutEach(_makeCbore, True, clean) + + #TODO: almost all code duplicated! + #but parameter list is different so a simple function pointer wont work + def cskHole(self, diameter, cskDiameter, cskAngle, depth=None, clean=True): + """ + Makes a countersunk hole for each item on the stack. + + :param diameter: the diameter of the hole + :type diameter: float > 0 + :param cskDiameter: the diameter of the countersink + :type cskDiameter: float > 0 and > diameter + :param cskAngle: angle of the countersink, in degrees ( 82 is common ) + :type cskAngle: float > 0 + :param depth: the depth of the hole + :type depth: float > 0 or None to drill thru the entire part. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + + The surface of the hole is at the current workplane. + + One hole is created for each item on the stack. A very common use case is to use a + construction rectangle to define the centers of a set of holes, like so:: + + s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane()\ + .rect(1.5,3.5,forConstruction=True)\ + .vertices().cskHole(0.125, 0.25,82,depth=None) + + This sample creates a plate with a set of holes at the corners. + + **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite + time consuming to create, but are quite easily defined by users. + + see :py:meth:`cboreHole` to make counterbores instead of countersinks + """ + + if depth is None: + depth = self.largestDimension() + + def _makeCsk(center): + #center is in local coordinates + + boreDir = Vector(0, 0, -1) + + #first make the hole + hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coords! + r = cskDiameter / 2.0 + h = r / math.tan(math.radians(cskAngle / 2.0)) + csk = Solid.makeCone(r, 0.0, h, center, boreDir) + r = hole.fuse(csk) + return r + + return self.cutEach(_makeCsk, True, clean) + + #TODO: almost all code duplicated! + #but parameter list is different so a simple function pointer wont work + def hole(self, diameter, depth=None, clean=True): + """ + Makes a hole for each item on the stack. + + :param diameter: the diameter of the hole + :type diameter: float > 0 + :param depth: the depth of the hole + :type depth: float > 0 or None to drill thru the entire part. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + + The surface of the hole is at the current workplane. + + One hole is created for each item on the stack. A very common use case is to use a + construction rectangle to define the centers of a set of holes, like so:: + + s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane()\ + .rect(1.5,3.5,forConstruction=True)\ + .vertices().hole(0.125, 0.25,82,depth=None) + + This sample creates a plate with a set of holes at the corners. + + **Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite + time consuming to create, but are quite easily defined by users. + + see :py:meth:`cboreHole` and :py:meth:`cskHole` to make counterbores or countersinks + """ + if depth is None: + depth = self.largestDimension() + + def _makeHole(center): + """ + Makes a single hole with counterbore at the supplied point + returns a solid suitable for subtraction + pnt is in local coordinates + """ + boreDir = Vector(0, 0, -1) + #first make the hole + hole = Solid.makeCylinder(diameter / 2.0, depth, center, boreDir) # local coordinates! + return hole + + return self.cutEach(_makeHole, True, clean) + + #TODO: duplicated code with _extrude and extrude + def twistExtrude(self, distance, angleDegrees, combine=True, clean=True): + """ + Extrudes a wire in the direction normal to the plane, but also twists by the specified + angle over the length of the extrusion + + The center point of the rotation will be the center of the workplane + + See extrude for more details, since this method is the same except for the the addition + of the angle. In fact, if angle=0, the result is the same as a linear extrude. + + **NOTE** This method can create complex calculations, so be careful using it with + complex geometries + + :param distance: the distance to extrude normal to the workplane + :param angle: angline ( in degrees) to rotate through the extrusion + :param boolean combine: True to combine the resulting solid with parent solids if found. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :return: a CQ object with the resulting solid selected. + """ + #group wires together into faces based on which ones are inside the others + #result is a list of lists + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) + + self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion + + #compute extrusion vector and extrude + eDir = self.plane.zDir.multiply(distance) + + #one would think that fusing faces into a compound and then extruding would work, + #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) + #but then cutting it from the main solid fails with BRep_NotDone. + #the work around is to extrude each and then join the resulting solids, which seems to work + + #underlying cad kernel can only handle simple bosses-- we'll aggregate them if there + # are multiple sets + r = None + for ws in wireSets: + thisObj = Solid.extrudeLinearWithRotation(ws[0], ws[1:], self.plane.origin, + eDir, angleDegrees) + if r is None: + r = thisObj + else: + r = r.fuse(thisObj) + + if combine: + newS = self._combineWithBase(r) + else: + newS = self.newObject([r]) + if clean: newS = newS.clean() + return newS + + def extrude(self, distance, combine=True, clean=True): + """ + Use all un-extruded wires in the parent chain to create a prismatic solid. + + :param distance: the distance to extrude, normal to the workplane plane + :type distance: float, negative means opposite the normal direction + :param boolean combine: True to combine the resulting solid with parent solids if found. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :return: a CQ object with the resulting solid selected. + + extrude always *adds* material to a part. + + The returned object is always a CQ object, and depends on wither combine is True, and + whether a context solid is already defined: + + * if combine is False, the new value is pushed onto the stack. + * if combine is true, the value is combined with the context solid if it exists, + and the resulting solid becomes the new context solid. + + FutureEnhancement: + Support for non-prismatic extrusion ( IE, sweeping along a profile, not just + perpendicular to the plane extrude to surface. this is quite tricky since the surface + selected may not be planar + """ + r = self._extrude(distance) # returns a Solid (or a compound if there were multiple) + if combine: + newS = self._combineWithBase(r) + else: + newS = self.newObject([r]) + if clean: newS = newS.clean() + return newS + + def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True): + """ + Use all un-revolved wires in the parent chain to create a solid. + + :param angleDegrees: the angle to revolve through. + :type angleDegrees: float, anything less than 360 degrees will leave the shape open + :param axisStart: the start point of the axis of rotation + :type axisStart: tuple, a two tuple + :param axisEnd: the end point of the axis of rotation + :type axisEnd: tuple, a two tuple + :param combine: True to combine the resulting solid with parent solids if found. + :type combine: boolean, combine with parent solid + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :return: a CQ object with the resulting solid selected. + + The returned object is always a CQ object, and depends on wither combine is True, and + whether a context solid is already defined: + + * if combine is False, the new value is pushed onto the stack. + * if combine is true, the value is combined with the context solid if it exists, + and the resulting solid becomes the new context solid. + """ + #Make sure we account for users specifying angles larger than 360 degrees + angleDegrees %= 360.0 + + #Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve + angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees + + # The default start point of the vector defining the axis of rotation will be the origin + # of the workplane + if axisStart is None: + axisStart = self.plane.toWorldCoords((0, 0)).toTuple() + else: + axisStart = self.plane.toWorldCoords(axisStart).toTuple() + + # The default end point of the vector defining the axis of rotation should be along the + # normal from the plane + if axisEnd is None: + # Make sure we match the user's assumed axis of rotation if they specified an start + # but not an end + if axisStart[1] != 0: + axisEnd = self.plane.toWorldCoords((0, axisStart[1])).toTuple() + else: + axisEnd = self.plane.toWorldCoords((0, 1)).toTuple() + else: + axisEnd = self.plane.toWorldCoords(axisEnd).toTuple() + + # returns a Solid (or a compound if there were multiple) + r = self._revolve(angleDegrees, axisStart, axisEnd) + if combine: + newS = self._combineWithBase(r) + else: + newS = self.newObject([r]) + if clean: newS = newS.clean() + return newS + + def _combineWithBase(self, obj): + """ + Combines the provided object with the base solid, if one can be found. + :param obj: + :return: a new object that represents the result of combining the base object with obj, + or obj if one could not be found + """ + baseSolid = self.findSolid(searchParents=True) + r = obj + if baseSolid is not None: + r = baseSolid.fuse(obj) + baseSolid.wrapped = r.wrapped + + return self.newObject([r]) + + def combine(self, clean=True): + """ + Attempts to combine all of the items on the stack into a single item. + WARNING: all of the items must be of the same type! + + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :raises: ValueError if there are no items on the stack, or if they cannot be combined + :return: a CQ object with the resulting object selected + """ + items = list(self.objects) + s = items.pop(0) + for ss in items: + s = s.fuse(ss) + + if clean: s = s.clean() + + return self.newObject([s]) + + def union(self, toUnion=None, combine=True, clean=True): + """ + Unions all of the items on the stack of toUnion with the current solid. + If there is no current solid, the items in toUnion are unioned together. + if combine=True, the result and the original are updated to point to the new object + if combine=False, the result will be on the stack, but the original is unmodified + + :param toUnion: + :type toUnion: a solid object, or a CQ object having a solid, + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :raises: ValueError if there is no solid to add to in the chain + :return: a CQ object with the resulting object selected + """ + + #first collect all of the items together + if type(toUnion) == CQ or type(toUnion) == Workplane: + solids = toUnion.solids().vals() + if len(solids) < 1: + raise ValueError("CQ object must have at least one solid on the stack to union!") + newS = solids.pop(0) + for s in solids: + newS = newS.fuse(s) + elif type(toUnion) == Solid: + newS = toUnion + else: + raise ValueError("Cannot union type '{}'".format(type(toUnion))) + + #now combine with existing solid, if there is one + # look for parents to cut from + solidRef = self.findSolid(searchStack=True, searchParents=True) + if combine and solidRef is not None: + r = solidRef.fuse(newS) + solidRef.wrapped = newS.wrapped + else: + r = newS + + if clean: r = r.clean() + + return self.newObject([r]) + + def cut(self, toCut, combine=True, clean=True): + """ + Cuts the provided solid from the current solid, IE, perform a solid subtraction + + if combine=True, the result and the original are updated to point to the new object + if combine=False, the result will be on the stack, but the original is unmodified + + :param toCut: object to cut + :type toCut: a solid object, or a CQ object having a solid, + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :raises: ValueError if there is no solid to subtract from in the chain + :return: a CQ object with the resulting object selected + """ + + # look for parents to cut from + solidRef = self.findSolid(searchStack=True, searchParents=True) + + if solidRef is None: + raise ValueError("Cannot find solid to cut from") + solidToCut = None + if type(toCut) == CQ or type(toCut) == Workplane: + solidToCut = toCut.val() + elif type(toCut) == Solid: + solidToCut = toCut + else: + raise ValueError("Cannot cut type '{}'".format(type(toCut))) + + newS = solidRef.cut(solidToCut) + + if clean: newS = newS.clean() + + if combine: + solidRef.wrapped = newS.wrapped + + return self.newObject([newS]) + + def cutBlind(self, distanceToCut, clean=True): + """ + Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. + + Similar to extrude, except that a solid in the parent chain is required to remove material + from. cutBlind always removes material from a part. + + :param distanceToCut: distance to extrude before cutting + :type distanceToCut: float, >0 means in the positive direction of the workplane normal, + <0 means in the negative direction + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :raises: ValueError if there is no solid to subtract from in the chain + :return: a CQ object with the resulting object selected + + see :py:meth:`cutThruAll` to cut material from the entire part + + Future Enhancements: + Cut Up to Surface + """ + #first, make the object + toCut = self._extrude(distanceToCut) + + #now find a solid in the chain + + solidRef = self.findSolid() + + s = solidRef.cut(toCut) + + if clean: s = s.clean() + + solidRef.wrapped = s.wrapped + return self.newObject([s]) + + def cutThruAll(self, positive=False, clean=True): + """ + Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid. + + Similar to extrude, except that a solid in the parent chain is required to remove material + from. cutThruAll always removes material from a part. + + :param boolean positive: True to cut in the positive direction, false to cut in the + negative direction + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :raises: ValueError if there is no solid to subtract from in the chain + :return: a CQ object with the resulting object selected + + see :py:meth:`cutBlind` to cut material to a limited depth + """ + maxDim = self.largestDimension() + if not positive: + maxDim *= (-1.0) + + return self.cutBlind(maxDim, clean) + + def loft(self, filled=True, ruled=False, combine=True): + """ + Make a lofted solid, through the set of wires. + :return: a CQ object containing the created loft + """ + wiresToLoft = self.ctx.pendingWires + self.ctx.pendingWires = [] + + r = Solid.makeLoft(wiresToLoft, ruled) + + if combine: + parentSolid = self.findSolid(searchStack=False, searchParents=True) + if parentSolid is not None: + r = parentSolid.fuse(r) + parentSolid.wrapped = r.wrapped + + return self.newObject([r]) + + def _extrude(self, distance): + """ + Make a prismatic solid from the existing set of pending wires. + + :param distance: distance to extrude + :return: a FreeCAD solid, suitable for boolean operations. + + This method is a utility method, primarily for plugin and internal use. + It is the basis for cutBlind,extrude,cutThruAll, and all similar methods. + + Future Enhancements: + extrude along a profile (sweep) + """ + + #group wires together into faces based on which ones are inside the others + #result is a list of lists + s = time.time() + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) + #print "sorted wires in %d sec" % ( time.time() - s ) + self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion + + #compute extrusion vector and extrude + eDir = self.plane.zDir.multiply(distance) + + + #one would think that fusing faces into a compound and then extruding would work, + #but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) + #but then cutting it from the main solid fails with BRep_NotDone. + #the work around is to extrude each and then join the resulting solids, which seems to work + + # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are + # multiple sets + + # IMPORTANT NOTE: OCC is slow slow slow in boolean operations. So you do NOT want to fuse + # each item to another and save the result-- instead, you want to combine all of the new + # items into a compound, and fuse them together!!! + # r = None + # for ws in wireSets: + # thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) + # if r is None: + # r = thisObj + # else: + # s = time.time() + # r = r.fuse(thisObj) + # print "Fused in %0.3f sec" % ( time.time() - s ) + # return r + + toFuse = [] + for ws in wireSets: + thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) + toFuse.append(thisObj) + + return Compound.makeCompound(toFuse) + + def _revolve(self, angleDegrees, axisStart, axisEnd): + """ + Make a solid from the existing set of pending wires. + + :param angleDegrees: the angle to revolve through. + :type angleDegrees: float, anything less than 360 degrees will leave the shape open + :param axisStart: the start point of the axis of rotation + :type axisStart: tuple, a two tuple + :param axisEnd: the end point of the axis of rotation + :type axisEnd: tuple, a two tuple + :return: a FreeCAD solid, suitable for boolean operations. + + This method is a utility method, primarily for plugin and internal use. + """ + #We have to gather the wires to be revolved + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) + + #Mark that all of the wires have been used to create a revolution + self.ctx.pendingWires = [] + + #Revolve the wires, make a compound out of them and then fuse them + toFuse = [] + for ws in wireSets: + thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd) + toFuse.append(thisObj) + + return Compound.makeCompound(toFuse) + + def box(self, length, width, height, centered=(True, True, True), combine=True, clean=True): + """ + Return a 3d box with specified dimensions for each object on the stack. + + :param length: box size in X direction + :type length: float > 0 + :param width: box size in Y direction + :type width: float > 0 + :param height: box size in Z direction + :type height: float > 0 + :param centered: should the box be centered, or should reference point be at the lower + bound of the range? + :param combine: should the results be combined with other solids on the stack + (and each other)? + :type combine: true to combine shapes, false otherwise. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + + Centered is a tuple that describes whether the box should be centered on the x,y, and + z axes. If true, the box is centered on the respective axis relative to the workplane + origin, if false, the workplane center will represent the lower bound of the resulting box + + one box is created for each item on the current stack. If no items are on the stack, one box + using the current workplane center is created. + + If combine is true, the result will be a single object on the stack: + if a solid was found in the chain, the result is that solid with all boxes produced + fused onto it otherwise, the result is the combination of all the produced boxes + + if combine is false, the result will be a list of the boxes produced + + Most often boxes form the basis for a part:: + + #make a single box with lower left corner at origin + s = Workplane().box(1,2,3,centered=(False,False,False) + + But sometimes it is useful to create an array of them: + + #create 4 small square bumps on a larger base plate: + s = Workplane().box(4,4,0.5).faces(">Z").workplane()\ + .rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True) + + """ + + def _makebox(pnt): + + #(xp,yp,zp) = self.plane.toLocalCoords(pnt) + (xp, yp, zp) = pnt.toTuple() + if centered[0]: + xp -= (length / 2.0) + if centered[1]: + yp -= (width / 2.0) + if centered[2]: + zp -= (height / 2.0) + + return Solid.makeBox(length, width, height, Vector(xp, yp, zp)) + + boxes = self.eachpoint(_makebox, True) + + #if combination is not desired, just return the created boxes + if not combine: + return boxes + else: + #combine everything + return self.union(boxes, clean=clean) + + def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, + centered=(True, True, True), combine=True, clean=True): + """ + Returns a 3D sphere with the specified radius for each point on the stack + + :param radius: The radius of the sphere + :type radius: float > 0 + :param direct: The direction axis for the creation of the sphere + :type direct: A three-tuple + :param angle1: The first angle to sweep the sphere arc through + :type angle1: float > 0 + :param angle2: The second angle to sweep the sphere arc through + :type angle2: float > 0 + :param angle3: The third angle to sweep the sphere arc through + :type angle3: float > 0 + :param centered: A three-tuple of booleans that determines whether the sphere is centered + on each axis origin + :param combine: Whether the results should be combined with other solids on the stack + (and each other) + :type combine: true to combine shapes, false otherwise + :return: A sphere object for each point on the stack + + Centered is a tuple that describes whether the sphere should be centered on the x,y, and + z axes. If true, the sphere is centered on the respective axis relative to the workplane + origin, if false, the workplane center will represent the lower bound of the resulting + sphere. + + One sphere is created for each item on the current stack. If no items are on the stack, one + box using the current workplane center is created. + + If combine is true, the result will be a single object on the stack: + If a solid was found in the chain, the result is that solid with all spheres produced + fused onto it otherwise, the result is the combination of all the produced boxes + + If combine is false, the result will be a list of the spheres produced + """ + + # Convert the direction tuple to a vector, if needed + if isinstance(direct, tuple): + direct = Vector(direct) + + def _makesphere(pnt): + """ + Inner function that is used to create a sphere for each point/object on the workplane + :param pnt: The center point for the sphere + :return: A CQ Solid object representing a sphere + """ + (xp, yp, zp) = pnt.toTuple() + + if not centered[0]: + xp += radius + + if not centered[1]: + yp += radius + + if not centered[2]: + zp += radius + + return Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3) + + # We want a sphere for each point on the workplane + spheres = self.eachpoint(_makesphere, True) + + # If we don't need to combine everything, just return the created spheres + if not combine: + return spheres + else: + return self.union(spheres, clean=clean) + + def clean(self): + """ + Cleans the current solid by removing unwanted edges from the + faces. + + Normally you don't have to call this function. It is + automatically called after each related operation. You can + disable this behavior with `clean=False` parameter if method + has any. In some cases this can improve performance + drastically but is generally dis-advised since it may break + some operations such as fillet. + + Note that in some cases where lots of solid operations are + chained, `clean()` may actually improve performance since + the shape is 'simplified' at each step and thus next operation + is easier. + + Also note that, due to limitation of the underlying engine, + `clean` may fail to produce a clean output in some cases such as + spherical faces. + """ + try: + cleanObjects = [obj.clean() for obj in self.objects] + except AttributeError: + raise AttributeError("%s object doesn't support `clean()` method!" % obj.ShapeType()) + return self.newObject(cleanObjects) diff --git a/build/lib.linux-i686-2.7/cadquery/cq_directive.py b/build/lib.linux-i686-2.7/cadquery/cq_directive.py new file mode 100644 index 0000000..0dc5fae --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/cq_directive.py @@ -0,0 +1,85 @@ +""" +A special directive for including a cq object. + +""" + +import traceback +from cadquery import * +from cadquery import cqgi +import StringIO +from docutils.parsers.rst import directives + +template = """ + +.. raw:: html + +
+ %(out_svg)s +
+
+
+ +""" +template_content_indent = ' ' + + +def cq_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + # only consider inline snippets + plot_code = '\n'.join(content) + + # Since we don't have a filename, use a hash based on the content + # the script must define a variable called 'out', which is expected to + # be a CQ object + out_svg = "Your Script Did not assign call build_output() function!" + + try: + _s = StringIO.StringIO() + result = cqgi.parse(plot_code).build() + + if result.success: + exporters.exportShape(result.first_result, "SVG", _s) + out_svg = _s.getvalue() + else: + raise result.exception + + except Exception: + traceback.print_exc() + out_svg = traceback.format_exc() + + # now out + # Now start generating the lines of output + lines = [] + + # get rid of new lines + out_svg = out_svg.replace('\n', '') + + txt_align = "left" + if "align" in options: + txt_align = options['align'] + + lines.extend((template % locals()).split('\n')) + + lines.extend(['::', '']) + lines.extend([' %s' % row.rstrip() + for row in plot_code.split('\n')]) + lines.append('') + + if len(lines): + state_machine.insert_input( + lines, state_machine.input_lines.source(0)) + + return [] + + +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir + + options = {'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'align': directives.unchanged + } + + app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options) diff --git a/build/lib.linux-i686-2.7/cadquery/cqgi.py b/build/lib.linux-i686-2.7/cadquery/cqgi.py new file mode 100644 index 0000000..92fd860 --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/cqgi.py @@ -0,0 +1,425 @@ +""" +The CadQuery Gateway Interface. +Provides classes and tools for executing CadQuery scripts +""" +import ast +import traceback +import time +import cadquery + +CQSCRIPT = "" + +def parse(script_source): + """ + Parses the script as a model, and returns a model. + + If you would prefer to access the underlying model without building it, + for example, to inspect its available parameters, construct a CQModel object. + + :param script_source: the script to run. Must be a valid cadquery script + :return: a CQModel object that defines the script and allows execution + + """ + model = CQModel(script_source) + return model + + +class CQModel(object): + """ + Represents a Cadquery Script. + + After construction, the metadata property contains + a ScriptMetaData object, which describes the model in more detail, + and can be used to retrive the parameters defined by the model. + + the build method can be used to generate a 3d model + """ + + def __init__(self, script_source): + """ + Create an object by parsing the supplied python script. + :param script_source: a python script to parse + """ + self.metadata = ScriptMetadata() + self.ast_tree = ast.parse(script_source, CQSCRIPT) + self.script_source = script_source + self._find_vars() + # TODO: pick up other scirpt metadata: + # describe + # pick up validation methods + + def _find_vars(self): + """ + Parse the script, and populate variables that appear to be + overridable. + """ + #assumption here: we assume that variable declarations + #are only at the top level of the script. IE, we'll ignore any + #variable definitions at lower levels of the script + + #we dont want to use the visit interface because here we excplicitly + #want to walk only the top level of the tree. + assignment_finder = ConstantAssignmentFinder(self.metadata) + + for node in self.ast_tree.body: + if isinstance(node, ast.Assign): + assignment_finder.visit_Assign(node) + + + def validate(self, params): + """ + Determine if the supplied parameters are valid. + NOT IMPLEMENTED YET-- raises NotImplementedError + :param params: a dictionary of parameters + + """ + raise NotImplementedError("not yet implemented") + + def build(self, build_parameters=None): + """ + Executes the script, using the optional parameters to override those in the model + :param build_parameters: a dictionary of variables. The variables must be + assignable to the underlying variable type. + :raises: Nothing. If there is an exception, it will be on the exception property of the result. + This is the interface so that we can return other information on the result, such as the build time + :return: a BuildResult object, which includes the status of the result, and either + a resulting shape or an exception + """ + if not build_parameters: + build_parameters = {} + + start = time.clock() + result = BuildResult() + + try: + self.set_param_values(build_parameters) + collector = ScriptCallback() + env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \ + .add_entry("build_object", collector.build_object).build() + + c = compile(self.ast_tree, CQSCRIPT, 'exec') + exec (c, env) + if collector.has_results(): + result.set_success_result(collector.outputObjects) + else: + raise NoOutputError("Script did not call build_object-- no output available.") + except Exception, ex: + print "Error Executing Script:" + result.set_failure_result(ex) + traceback.print_exc() + print "Full Text of Script:" + print self.script_source + + end = time.clock() + result.buildTime = end - start + return result + + def set_param_values(self, params): + model_parameters = self.metadata.parameters + + for k, v in params.iteritems(): + if k not in model_parameters: + raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k) + + p = model_parameters[k] + p.set_value(v) + + +class BuildResult(object): + """ + The result of executing a CadQuery script. + The success property contains whether the exeuction was successful. + + If successful, the results property contains a list of all results, + and the first_result property contains the first result. + + If unsuccessful, the exception property contains a reference to + the stack trace that occurred. + """ + def __init__(self): + self.buildTime = None + self.results = [] + self.first_result = None + self.success = False + self.exception = None + + def set_failure_result(self, ex): + self.exception = ex + self.success = False + + def set_success_result(self, results): + self.results = results + self.first_result = self.results[0] + self.success = True + + +class ScriptMetadata(object): + """ + Defines the metadata for a parsed CQ Script. + the parameters property is a dict of InputParameter objects. + """ + def __init__(self): + self.parameters = {} + + def add_script_parameter(self, p): + self.parameters[p.name] = p + + +class ParameterType(object): + pass + + +class NumberParameterType(ParameterType): + pass + + +class StringParameterType(ParameterType): + pass + + +class BooleanParameterType(ParameterType): + pass + + +class InputParameter: + """ + Defines a parameter that can be supplied when the model is executed. + + Name, varType, and default_value are always available, because they are computed + from a variable assignment line of code: + + The others are only available if the script has used define_parameter() to + provide additional metadata + + """ + def __init__(self): + + #: the default value for the variable. + self.default_value = None + + #: the name of the parameter. + self.name = None + + #: type of the variable: BooleanParameter, StringParameter, NumericParameter + self.varType = None + + #: help text describing the variable. Only available if the script used describe_parameter() + self.shortDesc = None + + + + #: valid values for the variable. Only available if the script used describe_parameter() + self.valid_values = [] + + + + self.ast_node = None + + @staticmethod + def create(ast_node, var_name, var_type, default_value, valid_values=None, short_desc=None): + + if valid_values is None: + valid_values = [] + + p = InputParameter() + p.ast_node = ast_node + p.default_value = default_value + p.name = var_name + if short_desc is None: + p.shortDesc = var_name + else: + p.shortDesc = short_desc + p.varType = var_type + p.valid_values = valid_values + return p + + def set_value(self, new_value): + + if len(self.valid_values) > 0 and new_value not in self.valid_values: + raise InvalidParameterError( + "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} " + .format(str(new_value), self.name, str(self.valid_values))) + + if self.varType == NumberParameterType: + try: + f = float(new_value) + self.ast_node.n = f + except ValueError: + raise InvalidParameterError( + "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric." + .format(str(new_value), self.name)) + + elif self.varType == StringParameterType: + self.ast_node.s = str(new_value) + elif self.varType == BooleanParameterType: + if new_value: + self.ast_node.id = 'True' + else: + self.ast_node.id = 'False' + else: + raise ValueError("Unknown Type of var: ", str(self.varType)) + + def __str__(self): + return "InputParameter: {name=%s, type=%s, defaultValue=%s" % ( + self.name, str(self.varType), str(self.default_value)) + + +class ScriptCallback(object): + """ + Allows a script to communicate with the container + the build_object() method is exposed to CQ scripts, to allow them + to return objects to the execution environment + """ + + def __init__(self): + self.outputObjects = [] + + def build_object(self, shape): + """ + return an object to the executing environment + :param shape: a cadquery object + """ + self.outputObjects.append(shape) + + def describe_parameter(self,var, valid_values, short_desc): + """ + Not yet implemented: allows a script to document + extra metadata about the parameters + """ + pass + + def add_error(self, param, field_list): + """ + Not implemented yet: allows scripts to indicate that there are problems with inputs + """ + pass + + def has_results(self): + return len(self.outputObjects) > 0 + + + +class InvalidParameterError(Exception): + """ + Raised when an attempt is made to provide a new parameter value + that cannot be assigned to the model + """ + pass + + +class NoOutputError(Exception): + """ + Raised when the script does not execute the build_object() method to + return a solid + """ + pass + + +class ScriptExecutionError(Exception): + """ + Represents a script syntax error. + Useful for helping clients pinpoint issues with the script + interactively + """ + + def __init__(self, line=None, message=None): + if line is None: + self.line = 0 + else: + self.line = line + + if message is None: + self.message = "Unknown Script Error" + else: + self.message = message + + def full_message(self): + return self.__repr__() + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return "ScriptError [Line %s]: %s" % (self.line, self.message) + + +class EnvironmentBuilder(object): + """ + Builds an execution environment for a cadquery script. + The environment includes the builtins, as well as + the other methods the script will need. + """ + def __init__(self): + self.env = {} + + def with_real_builtins(self): + return self.with_builtins(__builtins__) + + def with_builtins(self, env_dict): + self.env['__builtins__'] = env_dict + return self + + def with_cadquery_objects(self): + self.env['cadquery'] = cadquery + self.env['cq'] = cadquery + return self + + def add_entry(self, name, value): + self.env[name] = value + return self + + def build(self): + return self.env + + +class ConstantAssignmentFinder(ast.NodeTransformer): + """ + Visits a parse tree, and adds script parameters to the cqModel + """ + + def __init__(self, cq_model): + self.cqModel = cq_model + + def handle_assignment(self, var_name, value_node): + + + + try: + + if type(value_node) == ast.Num: + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, NumberParameterType, value_node.n)) + elif type(value_node) == ast.Str: + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, StringParameterType, value_node.s)) + elif type(value_node == ast.Name): + if value_node.id == 'True': + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, BooleanParameterType, True)) + elif value_node.id == 'False': + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, BooleanParameterType, True)) + except: + print "Unable to handle assignment for variable '%s'" % var_name + pass + + def visit_Assign(self, node): + + try: + left_side = node.targets[0] + + #do not handle attribute assignments + if isinstance(left_side,ast.Attribute): + return + + if type(node.value) in [ast.Num, ast.Str, ast.Name]: + self.handle_assignment(left_side.id, node.value) + elif type(node.value) == ast.Tuple: + # we have a multi-value assignment + for n, v in zip(left_side.elts, node.value.elts): + self.handle_assignment(n.id, v) + except: + traceback.print_exc() + print "Unable to handle assignment for node '%s'" % ast.dump(left_side) + + return node diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/__init__.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/__init__.py new file mode 100644 index 0000000..3e041e3 --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/__init__.py @@ -0,0 +1,112 @@ +""" + Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC + + This file is part of CadQuery. + + CadQuery is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + CadQuery is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see +""" +import os, sys + + +def _fc_path(): + """Find FreeCAD""" + _PATH = "" + if _PATH: + return _PATH + + #look for FREECAD_LIB env variable + if os.environ.has_key('FREECAD_LIB'): + _PATH = os.environ.get('FREECAD_LIB') + if os.path.exists( _PATH): + return _PATH + + if sys.platform.startswith('linux'): + #Make some dangerous assumptions... + for _PATH in [ + os.path.join(os.path.expanduser("~"), "lib/freecad/lib"), + "/usr/local/lib/freecad/lib", + "/usr/lib/freecad/lib", + "/opt/freecad/lib/", + "/usr/bin/freecad/lib", + "/usr/lib/freecad", + ]: + if os.path.exists(_PATH): + return _PATH + + elif sys.platform.startswith('win'): + #try all the usual suspects + for _PATH in [ + "c:/Program Files/FreeCAD0.12/bin", + "c:/Program Files/FreeCAD0.13/bin", + "c:/Program Files/FreeCAD0.14/bin", + "c:/Program Files/FreeCAD0.15/bin", + "c:/Program Files/FreeCAD0.16/bin", + "c:/Program Files/FreeCAD0.17/bin", + "c:/Program Files (x86)/FreeCAD0.12/bin", + "c:/Program Files (x86)/FreeCAD0.13/bin", + "c:/Program Files (x86)/FreeCAD0.14/bin", + "c:/Program Files (x86)/FreeCAD0.15/bin", + "c:/Program Files (x86)/FreeCAD0.16/bin", + "c:/Program Files (x86)/FreeCAD0.17/bin", + "c:/apps/FreeCAD0.12/bin", + "c:/apps/FreeCAD0.13/bin", + "c:/apps/FreeCAD0.14/bin", + "c:/apps/FreeCAD0.15/bin", + "c:/apps/FreeCAD0.16/bin", + "c:/apps/FreeCAD0.17/bin", + "c:/Program Files/FreeCAD 0.12/bin", + "c:/Program Files/FreeCAD 0.13/bin", + "c:/Program Files/FreeCAD 0.14/bin", + "c:/Program Files/FreeCAD 0.15/bin", + "c:/Program Files/FreeCAD 0.16/bin", + "c:/Program Files/FreeCAD 0.17/bin", + "c:/Program Files (x86)/FreeCAD 0.12/bin", + "c:/Program Files (x86)/FreeCAD 0.13/bin", + "c:/Program Files (x86)/FreeCAD 0.14/bin", + "c:/Program Files (x86)/FreeCAD 0.15/bin", + "c:/Program Files (x86)/FreeCAD 0.16/bin", + "c:/Program Files (x86)/FreeCAD 0.17/bin", + "c:/apps/FreeCAD 0.12/bin", + "c:/apps/FreeCAD 0.13/bin", + "c:/apps/FreeCAD 0.14/bin", + "c:/apps/FreeCAD 0.15/bin", + "c:/apps/FreeCAD 0.16/bin", + "c:/apps/FreeCAD 0.17/bin", + ]: + if os.path.exists(_PATH): + return _PATH + elif sys.platform.startswith('darwin'): + #Assume we're dealing with a Mac + for _PATH in [ + "/Applications/FreeCAD.app/Contents/lib", + os.path.join(os.path.expanduser("~"), "Library/Application Support/FreeCAD/lib"), + ]: + if os.path.exists(_PATH): + return _PATH + + + +#Make sure that the correct FreeCAD path shows up in Python's system path +no_library_path = ImportError('cadquery was unable to determine freecads library path') +try: + import FreeCAD +except ImportError: + path = _fc_path() + if path: + sys.path.insert(0, path) + try: + import FreeCAD + except ImportError: + raise no_library_path + else: raise no_library_path diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/exporters.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/exporters.py new file mode 100644 index 0000000..c4b097a --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/exporters.py @@ -0,0 +1,392 @@ +import cadquery + +import FreeCAD +import Drawing + +import tempfile, os, StringIO + + +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + + +class ExportTypes: + STL = "STL" + STEP = "STEP" + AMF = "AMF" + SVG = "SVG" + TJS = "TJS" + + +class UNITS: + MM = "mm" + IN = "in" + + +def toString(shape, exportType, tolerance=0.1): + s = StringIO.StringIO() + exportShape(shape, exportType, s, tolerance) + return s.getvalue() + + +def exportShape(shape,exportType,fileLike,tolerance=0.1): + """ + :param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery + object, the first value is exported + :param exportFormat: the exportFormat to use + :param tolerance: the tolerance, in model units + :param fileLike: a file like object to which the content will be written. + The object should be already open and ready to write. The caller is responsible + for closing the object + """ + + + if isinstance(shape,cadquery.CQ): + shape = shape.val() + + if exportType == ExportTypes.TJS: + #tessellate the model + tess = shape.tessellate(tolerance) + + mesher = JsonMesh() #warning: needs to be changed to remove buildTime and exportTime!!! + #add vertices + for vec in tess[0]: + mesher.addVertex(vec.x, vec.y, vec.z) + + #add faces + for f in tess[1]: + mesher.addTriangleFace(f[0],f[1], f[2]) + fileLike.write( mesher.toJson()) + elif exportType == ExportTypes.SVG: + fileLike.write(getSVG(shape.wrapped)) + elif exportType == ExportTypes.AMF: + tess = shape.tessellate(tolerance) + aw = AmfWriter(tess).writeAmf(fileLike) + else: + + #all these types required writing to a file and then + #re-reading. this is due to the fact that FreeCAD writes these + (h, outFileName) = tempfile.mkstemp() + #weird, but we need to close this file. the next step is going to write to + #it from c code, so it needs to be closed. + os.close(h) + + if exportType == ExportTypes.STEP: + shape.exportStep(outFileName) + elif exportType == ExportTypes.STL: + shape.wrapped.exportStl(outFileName) + else: + raise ValueError("No idea how i got here") + + res = readAndDeleteFile(outFileName) + fileLike.write(res) + +def readAndDeleteFile(fileName): + """ + read data from file provided, and delete it when done + return the contents as a string + """ + res = "" + with open(fileName,'r') as f: + res = f.read() + + os.remove(fileName) + return res + + +def guessUnitOfMeasure(shape): + """ + Guess the unit of measure of a shape. + """ + bb = shape.BoundBox + + dimList = [ bb.XLength, bb.YLength,bb.ZLength ] + #no real part would likely be bigger than 10 inches on any side + if max(dimList) > 10: + return UNITS.MM + + #no real part would likely be smaller than 0.1 mm on all dimensions + if min(dimList) < 0.1: + return UNITS.IN + + #no real part would have the sum of its dimensions less than about 5mm + if sum(dimList) < 10: + return UNITS.IN + + return UNITS.MM + + +class AmfWriter(object): + def __init__(self,tessellation): + + self.units = "mm" + self.tessellation = tessellation + + def writeAmf(self,outFile): + amf = ET.Element('amf',units=self.units) + #TODO: if result is a compound, we need to loop through them + object = ET.SubElement(amf,'object',id="0") + mesh = ET.SubElement(object,'mesh') + vertices = ET.SubElement(mesh,'vertices') + volume = ET.SubElement(mesh,'volume') + + #add vertices + for v in self.tessellation[0]: + vtx = ET.SubElement(vertices,'vertex') + coord = ET.SubElement(vtx,'coordinates') + x = ET.SubElement(coord,'x') + x.text = str(v.x) + y = ET.SubElement(coord,'y') + y.text = str(v.y) + z = ET.SubElement(coord,'z') + z.text = str(v.z) + + #add triangles + for t in self.tessellation[1]: + triangle = ET.SubElement(volume,'triangle') + v1 = ET.SubElement(triangle,'v1') + v1.text = str(t[0]) + v2 = ET.SubElement(triangle,'v2') + v2.text = str(t[1]) + v3 = ET.SubElement(triangle,'v3') + v3.text = str(t[2]) + + + ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1') + +""" + Objects that represent + three.js JSON object notation + https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0 +""" +class JsonMesh(object): + def __init__(self): + + self.vertices = []; + self.faces = []; + self.nVertices = 0; + self.nFaces = 0; + + def addVertex(self,x,y,z): + self.nVertices += 1; + self.vertices.extend([x,y,z]); + + #add triangle composed of the three provided vertex indices + def addTriangleFace(self, i,j,k): + #first position means justa simple triangle + self.nFaces += 1; + self.faces.extend([0,int(i),int(j),int(k)]); + + """ + Get a json model from this model. + For now we'll forget about colors, vertex normals, and all that stuff + """ + def toJson(self): + return JSON_TEMPLATE % { + 'vertices' : str(self.vertices), + 'faces' : str(self.faces), + 'nVertices': self.nVertices, + 'nFaces' : self.nFaces + }; + + +def getPaths(freeCadSVG): + """ + freeCad svg is worthless-- except for paths, which are fairly useful + this method accepts svg from fReeCAD and returns a list of strings suitable for inclusion in a path element + returns two lists-- one list of visible lines, and one list of hidden lines + + HACK ALERT!!!!! + FreeCAD does not give a way to determine which lines are hidden and which are not + the only way to tell is that hidden lines are in a with 0.15 stroke and visible are 0.35 stroke. + so we actually look for that as a way to parse. + + to make it worse, elementTree xpath attribute selectors do not work in python 2.6, and we + cannot use python 2.7 due to freecad. So its necessary to look for the pure strings! ick! + """ + + hiddenPaths = [] + visiblePaths = [] + if len(freeCadSVG) > 0: + #yuk, freecad returns svg fragments. stupid stupid + fullDoc = "%s" % freeCadSVG + e = ET.ElementTree(ET.fromstring(fullDoc)) + segments = e.findall(".//g") + for s in segments: + paths = s.findall("path") + + if s.get("stroke-width") == "0.15": #hidden line HACK HACK HACK + mylist = hiddenPaths + else: + mylist = visiblePaths + + for p in paths: + mylist.append(p.get("d")) + return (hiddenPaths,visiblePaths) + else: + return ([],[]) + + +def getSVG(shape,opts=None): + """ + Export a shape to SVG + """ + + d = {'width':800,'height':240,'marginLeft':200,'marginTop':20} + + if opts: + d.update(opts) + + #need to guess the scale and the coordinate center + uom = guessUnitOfMeasure(shape) + + width=float(d['width']) + height=float(d['height']) + marginLeft=float(d['marginLeft']) + marginTop=float(d['marginTop']) + + #TODO: provide option to give 3 views + viewVector = FreeCAD.Base.Vector(-1.75,1.1,5) + (visibleG0,visibleG1,hiddenG0,hiddenG1) = Drawing.project(shape,viewVector) + + (hiddenPaths,visiblePaths) = getPaths(Drawing.projectToSVG(shape,viewVector,"ShowHiddenLines")) #this param is totally undocumented! + + #get bounding box -- these are all in 2-d space + bb = visibleG0.BoundBox + bb.add(visibleG1.BoundBox) + bb.add(hiddenG0.BoundBox) + bb.add(hiddenG1.BoundBox) + + #width pixels for x, height pixesl for y + unitScale = min( width / bb.XLength * 0.75 , height / bb.YLength * 0.75 ) + + #compute amount to translate-- move the top left into view + (xTranslate,yTranslate) = ( (0 - bb.XMin) + marginLeft/unitScale ,(0- bb.YMax) - marginTop/unitScale) + + #compute paths ( again -- had to strip out freecad crap ) + hiddenContent = "" + for p in hiddenPaths: + hiddenContent += PATHTEMPLATE % p + + visibleContent = "" + for p in visiblePaths: + visibleContent += PATHTEMPLATE % p + + svg = SVG_TEMPLATE % ( + { + "unitScale" : str(unitScale), + "strokeWidth" : str(1.0/unitScale), + "hiddenContent" : hiddenContent , + "visibleContent" :visibleContent, + "xTranslate" : str(xTranslate), + "yTranslate" : str(yTranslate), + "width" : str(width), + "height" : str(height), + "textboxY" :str(height - 30), + "uom" : str(uom) + } + ) + #svg = SVG_TEMPLATE % ( + # {"content": projectedContent} + #) + return svg + + +def exportSVG(shape, fileName): + """ + accept a cadquery shape, and export it to the provided file + TODO: should use file-like objects, not a fileName, and/or be able to return a string instead + export a view of a part to svg + """ + + svg = getSVG(shape.val().wrapped) + f = open(fileName,'w') + f.write(svg) + f.close() + + + +JSON_TEMPLATE= """\ +{ + "metadata" : + { + "formatVersion" : 3, + "generatedBy" : "ParametricParts", + "vertices" : %(nVertices)d, + "faces" : %(nFaces)d, + "normals" : 0, + "colors" : 0, + "uvs" : 0, + "materials" : 1, + "morphTargets" : 0 + }, + + "scale" : 1.0, + + "materials": [ { + "DbgColor" : 15658734, + "DbgIndex" : 0, + "DbgName" : "Material", + "colorAmbient" : [0.0, 0.0, 0.0], + "colorDiffuse" : [0.6400000190734865, 0.10179081114814892, 0.126246120426746], + "colorSpecular" : [0.5, 0.5, 0.5], + "shading" : "Lambert", + "specularCoef" : 50, + "transparency" : 1.0, + "vertexColors" : false + }], + + "vertices": %(vertices)s, + + "morphTargets": [], + + "normals": [], + + "colors": [], + + "uvs": [[]], + + "faces": %(faces)s +} +""" + +SVG_TEMPLATE = """ + + + + +%(hiddenContent)s + + + + +%(visibleContent)s + + + + + X + + + Y + + + Z + + + +""" + +PATHTEMPLATE="\t\t\t\n" + diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/geom.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/geom.py new file mode 100644 index 0000000..2bd8c3a --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/geom.py @@ -0,0 +1,647 @@ +""" + Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC + + This file is part of CadQuery. + + CadQuery is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + CadQuery is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see +""" + +import math +import cadquery +import FreeCAD +import Part as FreeCADPart + + +def sortWiresByBuildOrder(wireList, plane, result=[]): + """Tries to determine how wires should be combined into faces. + + Assume: + The wires make up one or more faces, which could have 'holes' + Outer wires are listed ahead of inner wires + there are no wires inside wires inside wires + ( IE, islands -- we can deal with that later on ) + none of the wires are construction wires + + Compute: + one or more sets of wires, with the outer wire listed first, and inner + ones + + Returns, list of lists. + """ + result = [] + + remainingWires = list(wireList) + while remainingWires: + outerWire = remainingWires.pop(0) + group = [outerWire] + otherWires = list(remainingWires) + for w in otherWires: + if plane.isWireInside(outerWire, w): + group.append(w) + remainingWires.remove(w) + result.append(group) + + return result + + +class Vector(object): + """Create a 3-dimensional vector + + :param args: a 3-d vector, with x-y-z parts. + + you can either provide: + * nothing (in which case the null vector is return) + * a FreeCAD vector + * a vector ( in which case it is copied ) + * a 3-tuple + * three float values, x, y, and z + """ + def __init__(self, *args): + if len(args) == 3: + fV = FreeCAD.Base.Vector(args[0], args[1], args[2]) + elif len(args) == 1: + if isinstance(args[0], Vector): + fV = args[0].wrapped + elif isinstance(args[0], tuple): + fV = FreeCAD.Base.Vector(args[0][0], args[0][1], args[0][2]) + elif isinstance(args[0], FreeCAD.Base.Vector): + fV = args[0] + else: + fV = args[0] + elif len(args) == 0: + fV = FreeCAD.Base.Vector(0, 0, 0) + else: + raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple") + + self._wrapped = fV + + @property + def x(self): + return self.wrapped.x + + @property + def y(self): + return self.wrapped.y + + @property + def z(self): + return self.wrapped.z + + @property + def Length(self): + return self.wrapped.Length + + @property + def wrapped(self): + return self._wrapped + + def toTuple(self): + return (self.x, self.y, self.z) + + # TODO: is it possible to create a dynamic proxy without all this code? + def cross(self, v): + return Vector(self.wrapped.cross(v.wrapped)) + + def dot(self, v): + return self.wrapped.dot(v.wrapped) + + def sub(self, v): + return Vector(self.wrapped.sub(v.wrapped)) + + def add(self, v): + return Vector(self.wrapped.add(v.wrapped)) + + def multiply(self, scale): + """Return a copy multiplied by the provided scalar""" + tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped) + return Vector(tmp_fc_vector.multiply(scale)) + + def normalized(self): + """Return a normalized version of this vector""" + tmp_fc_vector = FreeCAD.Base.Vector(self.wrapped) + tmp_fc_vector.normalize() + return Vector(tmp_fc_vector) + + def Center(self): + """Return the vector itself + + The center of myself is myself. + Provided so that vectors, vertexes, and other shapes all support a + common interface, when Center() is requested for all objects on the + stack. + """ + return self + + def getAngle(self, v): + return self.wrapped.getAngle(v.wrapped) + + def distanceToLine(self): + raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") + + def projectToLine(self): + raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") + + def distanceToPlane(self): + raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") + + def projectToPlane(self): + raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") + + def __add__(self, v): + return self.add(v) + + def __repr__(self): + return self.wrapped.__repr__() + + def __str__(self): + return self.wrapped.__str__() + + def __ne__(self, other): + return self.wrapped.__ne__(other) + + def __eq__(self, other): + return self.wrapped.__eq__(other) + + +class Matrix: + """A 3d , 4x4 transformation matrix. + + Used to move geometry in space. + """ + def __init__(self, matrix=None): + if matrix is None: + self.wrapped = FreeCAD.Base.Matrix() + else: + self.wrapped = matrix + + def rotateX(self, angle): + self.wrapped.rotateX(angle) + + def rotateY(self, angle): + self.wrapped.rotateY(angle) + + +class Plane(object): + """A 2D coordinate system in space + + A 2D coordinate system in space, with the x-y axes on the plane, and a + particular point as the origin. + + A plane allows the use of 2-d coordinates, which are later converted to + global, 3d coordinates when the operations are complete. + + Frequently, it is not necessary to create work planes, as they can be + created automatically from faces. + """ + + @classmethod + def named(cls, stdName, origin=(0, 0, 0)): + """Create a predefined Plane based on the conventional names. + + :param stdName: one of (XY|YZ|ZX|XZ|YX|ZY|front|back|left|right|top|bottom) + :type stdName: string + :param origin: the desired origin, specified in global coordinates + :type origin: 3-tuple of the origin of the new plane, in global coorindates. + + Available named planes are as follows. Direction references refer to + the global directions. + + =========== ======= ======= ====== + Name xDir yDir zDir + =========== ======= ======= ====== + XY +x +y +z + YZ +y +z +x + ZX +z +x +y + XZ +x +z -y + YX +y +x -z + ZY +z +y -x + front +x +y +z + back -x +y -z + left +z +y -x + right -z +y +x + top +x -z +y + bottom +x +z -y + =========== ======= ======= ====== + """ + + namedPlanes = { + # origin, xDir, normal + 'XY': Plane(origin, (1, 0, 0), (0, 0, 1)), + 'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)), + 'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)), + 'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)), + 'YX': Plane(origin, (0, 1, 0), (0, 0, -1)), + 'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)), + 'front': Plane(origin, (1, 0, 0), (0, 0, 1)), + 'back': Plane(origin, (-1, 0, 0), (0, 0, -1)), + 'left': Plane(origin, (0, 0, 1), (-1, 0, 0)), + 'right': Plane(origin, (0, 0, -1), (1, 0, 0)), + 'top': Plane(origin, (1, 0, 0), (0, 1, 0)), + 'bottom': Plane(origin, (1, 0, 0), (0, -1, 0)) + } + + try: + return namedPlanes[stdName] + except KeyError: + raise ValueError('Supported names are {}'.format( + namedPlanes.keys())) + + @classmethod + def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): + plane = Plane.named('XY', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): + plane = Plane.named('YZ', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): + plane = Plane.named('ZX', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): + plane = Plane.named('XZ', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): + plane = Plane.named('YX', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): + plane = Plane.named('ZY', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): + plane = Plane.named('front', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)): + plane = Plane.named('back', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): + plane = Plane.named('left', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)): + plane = Plane.named('right', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): + plane = Plane.named('top', origin) + plane._setPlaneDir(xDir) + return plane + + @classmethod + def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): + plane = Plane.named('bottom', origin) + plane._setPlaneDir(xDir) + return plane + + def __init__(self, origin, xDir, normal): + """Create a Plane with an arbitrary orientation + + TODO: project x and y vectors so they work even if not orthogonal + :param origin: the origin + :type origin: a three-tuple of the origin, in global coordinates + :param xDir: a vector representing the xDirection. + :type xDir: a three-tuple representing a vector, or a FreeCAD Vector + :param normal: the normal direction for the new plane + :type normal: a FreeCAD Vector + :raises: ValueError if the specified xDir is not orthogonal to the provided normal. + :return: a plane in the global space, with the xDirection of the plane in the specified direction. + """ + normal = Vector(normal) + if (normal.Length == 0.0): + raise ValueError('normal should be non null') + self.zDir = normal.normalized() + xDir = Vector(xDir) + if (xDir.Length == 0.0): + raise ValueError('xDir should be non null') + self._setPlaneDir(xDir) + + self.invZDir = self.zDir.multiply(-1.0) + + self.origin = origin + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, value): + self._origin = Vector(value) + self._calcTransforms() + + def setOrigin2d(self, x, y): + """ + Set a new origin in the plane itself + + Set a new origin in the plane itself. The plane's orientation and + xDrection are unaffected. + + :param float x: offset in the x direction + :param float y: offset in the y direction + :return: void + + The new coordinates are specified in terms of the current 2-d system. + As an example: + + p = Plane.XY() + p.setOrigin2d(2, 2) + p.setOrigin2d(2, 2) + + results in a plane with its origin at (x, y) = (4, 4) in global + coordinates. Both operations were relative to local coordinates of the + plane. + """ + self.origin = self.toWorldCoords((x, y)) + + def isWireInside(self, baseWire, testWire): + """Determine if testWire is inside baseWire + + Determine if testWire is inside baseWire, after both wires are projected + into the current plane. + + :param baseWire: a reference wire + :type baseWire: a FreeCAD wire + :param testWire: another wire + :type testWire: a FreeCAD wire + :return: True if testWire is inside baseWire, otherwise False + + If either wire does not lie in the current plane, it is projected into + the plane first. + + *WARNING*: This method is not 100% reliable. It uses bounding box + tests, but needs more work to check for cases when curves are complex. + + Future Enhancements: + * Discretizing points along each curve to provide a more reliable + test. + """ + # TODO: also use a set of points along the wire to test as well. + # TODO: would it be more efficient to create objects in the local + # coordinate system, and then transform to global + # coordinates upon extrusion? + + tBaseWire = baseWire.transformGeometry(self.fG) + tTestWire = testWire.transformGeometry(self.fG) + + # These bounding boxes will have z=0, since we transformed them into the + # space of the plane. + bb = tBaseWire.BoundingBox() + tb = tTestWire.BoundingBox() + + # findOutsideBox actually inspects both ways, here we only want to + # know if one is inside the other + return bb == BoundBox.findOutsideBox2D(bb, tb) + + def toLocalCoords(self, obj): + """Project the provided coordinates onto this plane + + :param obj: an object or vector to convert + :type vector: a vector or shape + :return: an object of the same type, but converted to local coordinates + + + Most of the time, the z-coordinate returned will be zero, because most + operations based on a plane are all 2-d. Occasionally, though, 3-d + points outside of the current plane are transformed. One such example is + :py:meth:`Workplane.box`, where 3-d corners of a box are transformed to + orient the box in space correctly. + + """ + if isinstance(obj, Vector): + return Vector(self.fG.multiply(obj.wrapped)) + elif isinstance(obj, cadquery.Shape): + return obj.transformShape(self.rG) + else: + raise ValueError( + "Don't know how to convert type {} to local coordinates".format( + type(obj))) + + def toWorldCoords(self, tuplePoint): + """Convert a point in local coordinates to global coordinates + + :param tuplePoint: point in local coordinates to convert. + :type tuplePoint: a 2 or three tuple of float. The third value is taken to be zero if not supplied. + :return: a Vector in global coordinates + """ + if isinstance(tuplePoint, Vector): + v = tuplePoint + elif len(tuplePoint) == 2: + v = Vector(tuplePoint[0], tuplePoint[1], 0) + else: + v = Vector(tuplePoint) + return Vector(self.rG.multiply(v.wrapped)) + + def rotated(self, rotate=(0, 0, 0)): + """Returns a copy of this plane, rotated about the specified axes + + Since the z axis is always normal the plane, rotating around Z will + always produce a plane that is parallel to this one. + + The origin of the workplane is unaffected by the rotation. + + Rotations are done in order x, y, z. If you need a different order, + manually chain together multiple rotate() commands. + + :param rotate: Vector [xDegrees, yDegrees, zDegrees] + :return: a copy of this plane rotated as requested. + """ + rotate = Vector(rotate) + # Convert to radians. + rotate = rotate.multiply(math.pi / 180.0) + + # Compute rotation matrix. + m = FreeCAD.Base.Matrix() + m.rotateX(rotate.x) + m.rotateY(rotate.y) + m.rotateZ(rotate.z) + + # Compute the new plane. + newXdir = Vector(m.multiply(self.xDir.wrapped)) + newZdir = Vector(m.multiply(self.zDir.wrapped)) + + return Plane(self.origin, newXdir, newZdir) + + def rotateShapes(self, listOfShapes, rotationMatrix): + """Rotate the listOfShapes by the supplied rotationMatrix + + @param listOfShapes is a list of shape objects + @param rotationMatrix is a geom.Matrix object. + returns a list of shape objects rotated according to the rotationMatrix. + """ + # Compute rotation matrix (global --> local --> rotate --> global). + # rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG) + # rm = self.computeTransform(rotationMatrix) + + # There might be a better way, but to do this rotation takes 3 steps: + # - transform geometry to local coordinates + # - then rotate about x + # - then transform back to global coordinates. + + resultWires = [] + for w in listOfShapes: + mirrored = w.transformGeometry(rotationMatrix.wrapped) + + # If the first vertex of the second wire is not coincident with the + # first or last vertices of the first wire we have to fix the wire + # so that it will mirror correctly. + if ((mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[0].X and + mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[0].Y and + mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[0].Z) or + (mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[-1].X and + mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[-1].Y and + mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[-1].Z)): + + resultWires.append(mirrored) + else: + # Make sure that our mirrored edges meet up and are ordered + # properly. + aEdges = w.wrapped.Edges + aEdges.extend(mirrored.wrapped.Edges) + comp = FreeCADPart.Compound(aEdges) + mirroredWire = comp.connectEdgesToWires(False).Wires[0] + + resultWires.append(cadquery.Shape.cast(mirroredWire)) + + return resultWires + + def _setPlaneDir(self, xDir): + """Set the vectors parallel to the plane, i.e. xDir and yDir""" + if (self.zDir.dot(xDir) > 1e-5): + raise ValueError('xDir must be parralel to the plane') + xDir = Vector(xDir) + self.xDir = xDir.normalized() + self.yDir = self.zDir.cross(self.xDir).normalized() + + def _calcTransforms(self): + """Computes transformation matrices to convert between coordinates + + Computes transformation matrices to convert between local and global + coordinates. + """ + # r is the forward transformation matrix from world to local coordinates + # ok i will be really honest, i cannot understand exactly why this works + # something bout the order of the translation and the rotation. + # the double-inverting is strange, and I don't understand it. + r = FreeCAD.Base.Matrix() + + # Forward transform must rotate and adjust for origin. + (r.A11, r.A12, r.A13) = (self.xDir.x, self.xDir.y, self.xDir.z) + (r.A21, r.A22, r.A23) = (self.yDir.x, self.yDir.y, self.yDir.z) + (r.A31, r.A32, r.A33) = (self.zDir.x, self.zDir.y, self.zDir.z) + + invR = r.inverse() + invR.A14 = self.origin.x + invR.A24 = self.origin.y + invR.A34 = self.origin.z + + self.rG = invR + self.fG = invR.inverse() + + def computeTransform(self, tMatrix): + """Computes the 2-d projection of the supplied matrix""" + + return Matrix(self.fG.multiply(tMatrix.wrapped).multiply(self.rG)) + + +class BoundBox(object): + """A BoundingBox for an object or set of objects. Wraps the FreeCAD one""" + def __init__(self, bb): + self.wrapped = bb + self.xmin = bb.XMin + self.xmax = bb.XMax + self.xlen = bb.XLength + self.ymin = bb.YMin + self.ymax = bb.YMax + self.ylen = bb.YLength + self.zmin = bb.ZMin + self.zmax = bb.ZMax + self.zlen = bb.ZLength + self.center = Vector(bb.Center) + self.DiagonalLength = bb.DiagonalLength + + def add(self, obj): + """Returns a modified (expanded) bounding box + + obj can be one of several things: + 1. a 3-tuple corresponding to x,y, and z amounts to add + 2. a vector, containing the x,y,z values to add + 3. another bounding box, where a new box will be created that + encloses both. + + This bounding box is not changed. + """ + tmp = FreeCAD.Base.BoundBox(self.wrapped) + if isinstance(obj, tuple): + tmp.add(obj[0], obj[1], obj[2]) + elif isinstance(obj, Vector): + tmp.add(obj.fV) + elif isinstance(obj, BoundBox): + tmp.add(obj.wrapped) + + return BoundBox(tmp) + + @classmethod + def findOutsideBox2D(cls, b1, b2): + """Compares bounding boxes + + Compares bounding boxes. Returns none if neither is inside the other. + Returns the outer one if either is outside the other. + + BoundBox.isInside works in 3d, but this is a 2d bounding box, so it + doesn't work correctly plus, there was all kinds of rounding error in + the built-in implementation i do not understand. + """ + fc_bb1 = b1.wrapped + fc_bb2 = b2.wrapped + if (fc_bb1.XMin < fc_bb2.XMin and + fc_bb1.XMax > fc_bb2.XMax and + fc_bb1.YMin < fc_bb2.YMin and + fc_bb1.YMax > fc_bb2.YMax): + return b1 + + if (fc_bb2.XMin < fc_bb1.XMin and + fc_bb2.XMax > fc_bb1.XMax and + fc_bb2.YMin < fc_bb1.YMin and + fc_bb2.YMax > fc_bb1.YMax): + return b2 + + return None + + def isInside(self, anotherBox): + """Is the provided bounding box inside this one?""" + return self.wrapped.isInside(anotherBox.wrapped) diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/importers.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/importers.py new file mode 100644 index 0000000..7d4f0a9 --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/importers.py @@ -0,0 +1,71 @@ + +import cadquery +from .shapes import Shape + +import FreeCAD +import Part +import sys +import os +import urllib as urlreader +import tempfile + +class ImportTypes: + STEP = "STEP" + +class UNITS: + MM = "mm" + IN = "in" + + +def importShape(importType, fileName): + """ + Imports a file based on the type (STEP, STL, etc) + :param importType: The type of file that we're importing + :param fileName: THe name of the file that we're importing + """ + + #Check to see what type of file we're working with + if importType == ImportTypes.STEP: + return importStep(fileName) + + +#Loads a STEP file into a CQ.Workplane object +def importStep(fileName): + """ + Accepts a file name and loads the STEP file into a cadquery shape + :param fileName: The path and name of the STEP file to be imported + """ + #Now read and return the shape + try: + #print fileName + rshape = Part.read(fileName) + + #Make sure that we extract all the solids + solids = [] + for solid in rshape.Solids: + solids.append(Shape.cast(solid)) + + return cadquery.Workplane("XY").newObject(solids) + except: + raise ValueError("STEP File Could not be loaded") + +#Loads a STEP file from an URL into a CQ.Workplane object +def importStepFromURL(url): + #Now read and return the shape + try: + webFile = urlreader.urlopen(url) + tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False) + tempFile.write(webFile.read()) + webFile.close() + tempFile.close() + + rshape = Part.read(tempFile.name) + + #Make sure that we extract all the solids + solids = [] + for solid in rshape.Solids: + solids.append(Shape.cast(solid)) + + return cadquery.Workplane("XY").newObject(solids) + except: + raise ValueError("STEP File from the URL: " + url + " Could not be loaded") diff --git a/build/lib.linux-i686-2.7/cadquery/freecad_impl/shapes.py b/build/lib.linux-i686-2.7/cadquery/freecad_impl/shapes.py new file mode 100644 index 0000000..76af1c1 --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/freecad_impl/shapes.py @@ -0,0 +1,982 @@ +""" + Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC + + This file is part of CadQuery. + + CadQuery is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + CadQuery is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see + + Wrapper Classes for FreeCAD + These classes provide a stable interface for 3d objects, + independent of the FreeCAD interface. + + Future work might include use of pythonOCC, OCC, or even + another CAD kernel directly, so this interface layer is quite important. + + Funny, in java this is one of those few areas where i'd actually spend the time + to make an interface and an implementation, but for new these are just rolled together + + This interface layer provides three distinct values: + + 1. It allows us to avoid changing key api points if we change underlying implementations. + It would be a disaster if script and plugin authors had to change models because we + changed implementations + + 2. Allow better documentation. One of the reasons FreeCAD is no more popular is because + its docs are terrible. This allows us to provide good documentation via docstrings + for each wrapper + + 3. Work around bugs. there are a quite a feb bugs in free this layer allows fixing them + + 4. allows for enhanced functionality. Many objects are missing features we need. For example + we need a 'forConstruction' flag on the Wire object. this allows adding those kinds of things + + 5. allow changing interfaces when we'd like. there are few cases where the FreeCAD api is not + very user friendly: we like to change those when necessary. As an example, in the FreeCAD api, + all factory methods are on the 'Part' object, but it is very useful to know what kind of + object each one returns, so these are better grouped by the type of object they return. + (who would know that Part.makeCircle() returns an Edge, but Part.makePolygon() returns a Wire ? +""" +from cadquery import Vector, BoundBox +import FreeCAD +import Part as FreeCADPart + + +class Shape(object): + """ + Represents a shape in the system. + Wrappers the FreeCAD api + """ + + def __init__(self, obj): + self.wrapped = obj + self.forConstruction = False + + # Helps identify this solid through the use of an ID + self.label = "" + + @classmethod + def cast(cls, obj, forConstruction=False): + "Returns the right type of wrapper, given a FreeCAD object" + s = obj.ShapeType + if type(obj) == FreeCAD.Base.Vector: + return Vector(obj) + tr = None + + # TODO: there is a clever way to do this i'm sure with a lookup + # but it is not a perfect mapping, because we are trying to hide + # a bit of the complexity of Compounds in FreeCAD. + if s == 'Vertex': + tr = Vertex(obj) + elif s == 'Edge': + tr = Edge(obj) + elif s == 'Wire': + tr = Wire(obj) + elif s == 'Face': + tr = Face(obj) + elif s == 'Shell': + tr = Shell(obj) + elif s == 'Solid': + tr = Solid(obj) + elif s == 'Compound': + #compound of solids, lets return a solid instead + if len(obj.Solids) > 1: + tr = Solid(obj) + elif len(obj.Solids) == 1: + tr = Solid(obj.Solids[0]) + elif len(obj.Wires) > 0: + tr = Wire(obj) + else: + tr = Compound(obj) + else: + raise ValueError("cast:unknown shape type %s" % s) + + tr.forConstruction = forConstruction + return tr + + # TODO: all these should move into the exporters folder. + # we dont need a bunch of exporting code stored in here! + # + def exportStl(self, fileName): + self.wrapped.exportStl(fileName) + + def exportStep(self, fileName): + self.wrapped.exportStep(fileName) + + def exportShape(self, fileName, fileFormat): + if fileFormat == ExportFormats.STL: + self.wrapped.exportStl(fileName) + elif fileFormat == ExportFormats.BREP: + self.wrapped.exportBrep(fileName) + elif fileFormat == ExportFormats.STEP: + self.wrapped.exportStep(fileName) + elif fileFormat == ExportFormats.AMF: + # not built into FreeCAD + #TODO: user selected tolerance + tess = self.wrapped.tessellate(0.1) + aw = amfUtils.AMFWriter(tess) + aw.writeAmf(fileName) + elif fileFormat == ExportFormats.IGES: + self.wrapped.exportIges(fileName) + else: + raise ValueError("Unknown export format: %s" % format) + + def geomType(self): + """ + Gets the underlying geometry type + :return: a string according to the geometry type. + + Implementations can return any values desired, but the + values the user uses in type filters should correspond to these. + + As an example, if a user does:: + + CQ(object).faces("%mytype") + + The expectation is that the geomType attribute will return 'mytype' + + The return values depend on the type of the shape: + + Vertex: always 'Vertex' + Edge: LINE, ARC, CIRCLE, SPLINE + Face: PLANE, SPHERE, CONE + Solid: 'Solid' + Shell: 'Shell' + Compound: 'Compound' + Wire: 'Wire' + """ + return self.wrapped.ShapeType + + def isType(self, obj, strType): + """ + Returns True if the shape is the specified type, false otherwise + + contrast with ShapeType, which will raise an exception + if the provide object is not a shape at all + """ + if hasattr(obj, 'ShapeType'): + return obj.ShapeType == strType + else: + return False + + def hashCode(self): + return self.wrapped.hashCode() + + def isNull(self): + return self.wrapped.isNull() + + def isSame(self, other): + return self.wrapped.isSame(other.wrapped) + + def isEqual(self, other): + return self.wrapped.isEqual(other.wrapped) + + def isValid(self): + return self.wrapped.isValid() + + def BoundingBox(self): + return BoundBox(self.wrapped.BoundBox) + + def Center(self): + # A Part.Shape object doesn't have the CenterOfMass function, but it's wrapped Solid(s) does + if isinstance(self.wrapped, FreeCADPart.Shape): + # If there are no Solids, we're probably dealing with a Face or something similar + if len(self.Solids()) == 0: + return Vector(self.wrapped.CenterOfMass) + elif len(self.Solids()) == 1: + return Vector(self.Solids()[0].wrapped.CenterOfMass) + elif len(self.Solids()) > 1: + return self.CombinedCenter(self.Solids()) + elif isinstance(self.wrapped, FreeCADPart.Solid): + return Vector(self.wrapped.CenterOfMass) + else: + raise ValueError("Cannot find the center of %s object type" % str(type(self.Solids()[0].wrapped))) + + def CenterOfBoundBox(self): + if isinstance(self.wrapped, FreeCADPart.Shape): + # If there are no Solids, we're probably dealing with a Face or something similar + if len(self.Solids()) == 0: + return Vector(self.wrapped.BoundBox.Center) + elif len(self.Solids()) == 1: + return Vector(self.Solids()[0].wrapped.BoundBox.Center) + elif len(self.Solids()) > 1: + return self.CombinedCenterOfBoundBox(self.Solids()) + elif isinstance(self.wrapped, FreeCADPart.Solid): + return Vector(self.wrapped.BoundBox.Center) + else: + raise ValueError("Cannot find the center(BoundBox's) of %s object type" % str(type(self.Solids()[0].wrapped))) + + @staticmethod + def CombinedCenter(objects): + """ + Calculates the center of mass of multiple objects. + + :param objects: a list of objects with mass + """ + total_mass = sum(o.wrapped.Mass for o in objects) + weighted_centers = [o.wrapped.CenterOfMass.multiply(o.wrapped.Mass) for o in objects] + + sum_wc = weighted_centers[0] + for wc in weighted_centers[1:] : + sum_wc = sum_wc.add(wc) + + return Vector(sum_wc.multiply(1./total_mass)) + + @staticmethod + def CombinedCenterOfBoundBox(objects): + """ + Calculates the center of BoundBox of multiple objects. + + :param objects: a list of objects with mass 1 + """ + total_mass = len(objects) + weighted_centers = [o.wrapped.BoundBox.Center.multiply(1.0) for o in objects] + + sum_wc = weighted_centers[0] + for wc in weighted_centers[1:] : + sum_wc = sum_wc.add(wc) + + return Vector(sum_wc.multiply(1./total_mass)) + + def Closed(self): + return self.wrapped.Closed + + def ShapeType(self): + return self.wrapped.ShapeType + + def Vertices(self): + return [Vertex(i) for i in self.wrapped.Vertexes] + + def Edges(self): + return [Edge(i) for i in self.wrapped.Edges] + + def Compounds(self): + return [Compound(i) for i in self.wrapped.Compounds] + + def Wires(self): + return [Wire(i) for i in self.wrapped.Wires] + + def Faces(self): + return [Face(i) for i in self.wrapped.Faces] + + def Shells(self): + return [Shell(i) for i in self.wrapped.Shells] + + def Solids(self): + return [Solid(i) for i in self.wrapped.Solids] + + def Area(self): + return self.wrapped.Area + + def Length(self): + return self.wrapped.Length + + def rotate(self, startVector, endVector, angleDegrees): + """ + Rotates a shape around an axis + :param startVector: start point of rotation axis either a 3-tuple or a Vector + :param endVector: end point of rotation axis, either a 3-tuple or a Vector + :param angleDegrees: angle to rotate, in degrees + :return: a copy of the shape, rotated + """ + if type(startVector) == tuple: + startVector = Vector(startVector) + + if type(endVector) == tuple: + endVector = Vector(endVector) + + tmp = self.wrapped.copy() + tmp.rotate(startVector.wrapped, endVector.wrapped, angleDegrees) + return Shape.cast(tmp) + + def translate(self, vector): + + if type(vector) == tuple: + vector = Vector(vector) + tmp = self.wrapped.copy() + tmp.translate(vector.wrapped) + return Shape.cast(tmp) + + def scale(self, factor): + tmp = self.wrapped.copy() + tmp.scale(factor) + return Shape.cast(tmp) + + def copy(self): + return Shape.cast(self.wrapped.copy()) + + def transformShape(self, tMatrix): + """ + tMatrix is a matrix object. + returns a copy of the ojbect, transformed by the provided matrix, + with all objects keeping their type + """ + tmp = self.wrapped.copy() + tmp.transformShape(tMatrix) + r = Shape.cast(tmp) + r.forConstruction = self.forConstruction + return r + + def transformGeometry(self, tMatrix): + """ + tMatrix is a matrix object. + + returns a copy of the object, but with geometry transformed insetad of just + rotated. + + WARNING: transformGeometry will sometimes convert lines and circles to splines, + but it also has the ability to handle skew and stretching transformations. + + If your transformation is only translation and rotation, it is safer to use transformShape, + which doesnt change the underlying type of the geometry, but cannot handle skew transformations + """ + tmp = self.wrapped.copy() + tmp = tmp.transformGeometry(tMatrix) + return Shape.cast(tmp) + + def __hash__(self): + return self.wrapped.hashCode() + + +class Vertex(Shape): + """ + A Single Point in Space + """ + + def __init__(self, obj, forConstruction=False): + """ + Create a vertex from a FreeCAD Vertex + """ + self.wrapped = obj + self.forConstruction = forConstruction + self.X = obj.X + self.Y = obj.Y + self.Z = obj.Z + + # Helps identify this solid through the use of an ID + self.label = "" + + def toTuple(self): + return (self.X, self.Y, self.Z) + + def Center(self): + """ + The center of a vertex is itself! + """ + return Vector(self.wrapped.Point) + + +class Edge(Shape): + """ + A trimmed curve that represents the border of a face + """ + + def __init__(self, obj): + """ + An Edge + """ + self.wrapped = obj + # self.startPoint = None + # self.endPoint = None + + self.edgetypes = { + FreeCADPart.Line: 'LINE', + FreeCADPart.ArcOfCircle: 'ARC', + FreeCADPart.Circle: 'CIRCLE' + } + + # Helps identify this solid through the use of an ID + self.label = "" + + def geomType(self): + t = type(self.wrapped.Curve) + if self.edgetypes.has_key(t): + return self.edgetypes[t] + else: + return "Unknown Edge Curve Type: %s" % str(t) + + def startPoint(self): + """ + + :return: a vector representing the start poing of this edge + + Note, circles may have the start and end points the same + """ + # work around freecad bug where valueAt is unreliable + curve = self.wrapped.Curve + return Vector(curve.value(self.wrapped.ParameterRange[0])) + + def endPoint(self): + """ + + :return: a vector representing the end point of this edge. + + Note, circles may have the start and end points the same + + """ + # warning: easier syntax in freecad of .valueAt(.ParameterRange[1]) has + # a bug with curves other than arcs, but using the underlying curve directly seems to work + # that's the solution i'm using below + curve = self.wrapped.Curve + v = Vector(curve.value(self.wrapped.ParameterRange[1])) + return v + + def tangentAt(self, locationVector=None): + """ + Compute tangent vector at the specified location. + :param locationVector: location to use. Use the center point if None + :return: tangent vector + """ + if locationVector is None: + locationVector = self.Center() + + p = self.wrapped.Curve.parameter(locationVector.wrapped) + return Vector(self.wrapped.tangentAt(p)) + + @classmethod + def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360): + return Edge(FreeCADPart.makeCircle(radius, toVector(pnt), toVector(dir), angle1, angle2)) + + @classmethod + def makeSpline(cls, listOfVector): + """ + Interpolate a spline through the provided points. + :param cls: + :param listOfVector: a list of Vectors that represent the points + :return: an Edge + """ + vecs = [v.wrapped for v in listOfVector] + + spline = FreeCADPart.BSplineCurve() + spline.interpolate(vecs, False) + return Edge(spline.toShape()) + + @classmethod + def makeThreePointArc(cls, v1, v2, v3): + """ + Makes a three point arc through the provided points + :param cls: + :param v1: start vector + :param v2: middle vector + :param v3: end vector + :return: an edge object through the three points + """ + arc = FreeCADPart.Arc(v1.wrapped, v2.wrapped, v3.wrapped) + e = Edge(arc.toShape()) + return e # arcane and undocumented, this creates an Edge object + + @classmethod + def makeLine(cls, v1, v2): + """ + Create a line between two points + :param v1: Vector that represents the first point + :param v2: Vector that represents the second point + :return: A linear edge between the two provided points + """ + return Edge(FreeCADPart.makeLine(v1.toTuple(), v2.toTuple())) + + +class Wire(Shape): + """ + A series of connected, ordered Edges, that typically bounds a Face + """ + + def __init__(self, obj): + """ + A Wire + """ + self.wrapped = obj + + # Helps identify this solid through the use of an ID + self.label = "" + + @classmethod + def combine(cls, listOfWires): + """ + Attempt to combine a list of wires into a new wire. + the wires are returned in a list. + :param cls: + :param listOfWires: + :return: + """ + return Shape.cast(FreeCADPart.Wire([w.wrapped for w in listOfWires])) + + @classmethod + def assembleEdges(cls, listOfEdges): + """ + Attempts to build a wire that consists of the edges in the provided list + :param cls: + :param listOfEdges: a list of Edge objects + :return: a wire with the edges assembled + """ + fCEdges = [a.wrapped for a in listOfEdges] + + wa = Wire(FreeCADPart.Wire(fCEdges)) + return wa + + @classmethod + def makeCircle(cls, radius, center, normal): + """ + Makes a Circle centered at the provided point, having normal in the provided direction + :param radius: floating point radius of the circle, must be > 0 + :param center: vector representing the center of the circle + :param normal: vector representing the direction of the plane the circle should lie in + :return: + """ + w = Wire(FreeCADPart.Wire([FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped)])) + return w + + @classmethod + def makePolygon(cls, listOfVertices, forConstruction=False): + # convert list of tuples into Vectors. + w = Wire(FreeCADPart.makePolygon([i.wrapped for i in listOfVertices])) + w.forConstruction = forConstruction + return w + + @classmethod + def makeHelix(cls, pitch, height, radius, angle=360.0): + """ + Make a helix with a given pitch, height and radius + By default a cylindrical surface is used to create the helix. If + the fourth parameter is set (the apex given in degree) a conical surface is used instead' + """ + return Wire(FreeCADPart.makeHelix(pitch, height, radius, angle)) + + def clean(self): + """This method is not implemented yet.""" + return self + +class Face(Shape): + """ + a bounded surface that represents part of the boundary of a solid + """ + def __init__(self, obj): + + self.wrapped = obj + + self.facetypes = { + # TODO: bezier,bspline etc + FreeCADPart.Plane: 'PLANE', + FreeCADPart.Sphere: 'SPHERE', + FreeCADPart.Cone: 'CONE' + } + + # Helps identify this solid through the use of an ID + self.label = "" + + def geomType(self): + t = type(self.wrapped.Surface) + if self.facetypes.has_key(t): + return self.facetypes[t] + else: + return "Unknown Face Surface Type: %s" % str(t) + + def normalAt(self, locationVector=None): + """ + Computes the normal vector at the desired location on the face. + + :returns: a vector representing the direction + :param locationVector: the location to compute the normal at. If none, the center of the face is used. + :type locationVector: a vector that lies on the surface. + """ + if locationVector == None: + locationVector = self.Center() + (u, v) = self.wrapped.Surface.parameter(locationVector.wrapped) + + return Vector(self.wrapped.normalAt(u, v).normalize()) + + @classmethod + def makePlane(cls, length, width, basePnt=None, dir=None): + return Face(FreeCADPart.makePlan(length, width, toVector(basePnt), toVector(dir))) + + @classmethod + def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None): + """ + 'makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface + Create a ruled surface out of two edges or wires. If wires are used then + these must have the same + """ + return Shape.cast(FreeCADPart.makeRuledSurface(edgeOrWire1.obj, edgeOrWire2.obj, dist)) + + def cut(self, faceToCut): + "Remove a face from another one" + return Shape.cast(self.obj.cut(faceToCut.obj)) + + def fuse(self, faceToJoin): + return Shape.cast(self.obj.fuse(faceToJoin.obj)) + + def intersect(self, faceToIntersect): + """ + computes the intersection between the face and the supplied one. + The result could be a face or a compound of faces + """ + return Shape.cast(self.obj.common(faceToIntersect.obj)) + + +class Shell(Shape): + """ + the outer boundary of a surface + """ + def __init__(self, wrapped): + """ + A Shell + """ + self.wrapped = wrapped + + # Helps identify this solid through the use of an ID + self.label = "" + + @classmethod + def makeShell(cls, listOfFaces): + return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces])) + + +class Solid(Shape): + """ + a single solid + """ + def __init__(self, obj): + """ + A Solid + """ + self.wrapped = obj + + # Helps identify this solid through the use of an ID + self.label = "" + + @classmethod + def isSolid(cls, obj): + """ + Returns true if the object is a FreeCAD solid, false otherwise + """ + if hasattr(obj, 'ShapeType'): + if obj.ShapeType == 'Solid' or \ + (obj.ShapeType == 'Compound' and len(obj.Solids) > 0): + return True + return False + + @classmethod + def makeBox(cls, length, width, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)): + """ + makeBox(length,width,height,[pnt,dir]) -- Make a box located in pnt with the dimensions (length,width,height) + By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)' + """ + return Shape.cast(FreeCADPart.makeBox(length, width, height, pnt.wrapped, dir.wrapped)) + + @classmethod + def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360): + """ + Make a cone with given radii and height + By default pnt=Vector(0,0,0), + dir=Vector(0,0,1) and angle=360' + """ + return Shape.cast(FreeCADPart.makeCone(radius1, radius2, height, pnt.wrapped, dir.wrapped, angleDegrees)) + + @classmethod + def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360): + """ + makeCylinder(radius,height,[pnt,dir,angle]) -- + Make a cylinder with a given radius and height + By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360' + """ + return Shape.cast(FreeCADPart.makeCylinder(radius, height, pnt.wrapped, dir.wrapped, angleDegrees)) + + @classmethod + def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None): + """ + makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) -- + Make a torus with agiven radii and angles + By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0 + ,angle1=360 and angle=360' + """ + return Shape.cast(FreeCADPart.makeTorus(radius1, radius2, pnt, dir, angleDegrees1, angleDegrees2)) + + @classmethod + def sweep(cls, profileWire, pathWire): + """ + make a solid by sweeping the profileWire along the specified path + :param cls: + :param profileWire: + :param pathWire: + :return: + """ + # needs to use freecad wire.makePipe or makePipeShell + # needs to allow free-space wires ( those not made from a workplane ) + + @classmethod + def makeLoft(cls, listOfWire, ruled=False): + """ + makes a loft from a list of wires + The wires will be converted into faces when possible-- it is presumed that nobody ever actually + wants to make an infinitely thin shell for a real FreeCADPart. + """ + # the True flag requests building a solid instead of a shell. + + return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True, ruled)) + + @classmethod + def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None): + """ + Make a wedge located in pnt + By default pnt=Vector(0,0,0) and dir=Vector(0,0,1) + """ + return Shape.cast( + FreeCADPart.makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt, dir)) + + @classmethod + def makeSphere(cls, radius, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None): + """ + Make a sphere with a given radius + By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360 + """ + return Shape.cast(FreeCADPart.makeSphere(radius, pnt.wrapped, dir.wrapped, angleDegrees1, angleDegrees2, angleDegrees3)) + + @classmethod + def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees): + """ + Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector. + + Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the + construction methods used here are different enough that they should be separate. + + At a high level, the steps followed are: + (1) accept a set of wires + (2) create another set of wires like this one, but which are transformed and rotated + (3) create a ruledSurface between the sets of wires + (4) create a shell and compute the resulting object + + :param outerWire: the outermost wire, a cad.Wire + :param innerWires: a list of inner wires, a list of cad.Wire + :param vecCenter: the center point about which to rotate. the axis of rotation is defined by + vecNormal, located at vecCenter. ( a cad.Vector ) + :param vecNormal: a vector along which to extrude the wires ( a cad.Vector ) + :param angleDegrees: the angle to rotate through while extruding + :return: a cad.Solid object + """ + + # from this point down we are dealing with FreeCAD wires not cad.wires + startWires = [outerWire.wrapped] + [i.wrapped for i in innerWires] + endWires = [] + p1 = vecCenter.wrapped + p2 = vecCenter.add(vecNormal).wrapped + + # make translated and rotated copy of each wire + for w in startWires: + w2 = w.copy() + w2.translate(vecNormal.wrapped) + w2.rotate(p1, p2, angleDegrees) + endWires.append(w2) + + # make a ruled surface for each set of wires + sides = [] + for w1, w2 in zip(startWires, endWires): + rs = FreeCADPart.makeRuledSurface(w1, w2) + sides.append(rs) + + #make faces for the top and bottom + startFace = FreeCADPart.Face(startWires) + endFace = FreeCADPart.Face(endWires) + + #collect all the faces from the sides + faceList = [startFace] + for s in sides: + faceList.extend(s.Faces) + faceList.append(endFace) + + shell = FreeCADPart.makeShell(faceList) + solid = FreeCADPart.makeSolid(shell) + return Shape.cast(solid) + + @classmethod + def extrudeLinear(cls, outerWire, innerWires, vecNormal): + """ + Attempt to extrude the list of wires into a prismatic solid in the provided direction + + :param outerWire: the outermost wire + :param innerWires: a list of inner wires + :param vecNormal: a vector along which to extrude the wires + :return: a Solid object + + The wires must not intersect + + Extruding wires is very non-trivial. Nested wires imply very different geometry, and + there are many geometries that are invalid. In general, the following conditions must be met: + + * all wires must be closed + * there cannot be any intersecting or self-intersecting wires + * wires must be listed from outside in + * more than one levels of nesting is not supported reliably + + This method will attempt to sort the wires, but there is much work remaining to make this method + reliable. + """ + + # one would think that fusing faces into a compound and then extruding would work, + # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc), + # but then cutting it from the main solid fails with BRep_NotDone. + #the work around is to extrude each and then join the resulting solids, which seems to work + + #FreeCAD allows this in one operation, but others might not + freeCADWires = [outerWire.wrapped] + for w in innerWires: + freeCADWires.append(w.wrapped) + + f = FreeCADPart.Face(freeCADWires) + result = f.extrude(vecNormal.wrapped) + + return Shape.cast(result) + + @classmethod + def revolve(cls, outerWire, innerWires, angleDegrees, axisStart, axisEnd): + """ + Attempt to revolve the list of wires into a solid in the provided direction + + :param outerWire: the outermost wire + :param innerWires: a list of inner wires + :param angleDegrees: the angle to revolve through. + :type angleDegrees: float, anything less than 360 degrees will leave the shape open + :param axisStart: the start point of the axis of rotation + :type axisStart: tuple, a two tuple + :param axisEnd: the end point of the axis of rotation + :type axisEnd: tuple, a two tuple + :return: a Solid object + + The wires must not intersect + + * all wires must be closed + * there cannot be any intersecting or self-intersecting wires + * wires must be listed from outside in + * more than one levels of nesting is not supported reliably + * the wire(s) that you're revolving cannot be centered + + This method will attempt to sort the wires, but there is much work remaining to make this method + reliable. + """ + freeCADWires = [outerWire.wrapped] + + for w in innerWires: + freeCADWires.append(w.wrapped) + + f = FreeCADPart.Face(freeCADWires) + + rotateCenter = FreeCAD.Base.Vector(axisStart) + rotateAxis = FreeCAD.Base.Vector(axisEnd) + + #Convert our axis end vector into to something FreeCAD will understand (an axis specification vector) + rotateAxis = rotateCenter.sub(rotateAxis) + + #FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation + result = f.revolve(rotateCenter, rotateAxis, angleDegrees) + + return Shape.cast(result) + + def tessellate(self, tolerance): + return self.wrapped.tessellate(tolerance) + + def intersect(self, toIntersect): + """ + computes the intersection between this solid and the supplied one + The result could be a face or a compound of faces + """ + return Shape.cast(self.wrapped.common(toIntersect.wrapped)) + + def cut(self, solidToCut): + "Remove a solid from another one" + return Shape.cast(self.wrapped.cut(solidToCut.wrapped)) + + def fuse(self, solidToJoin): + return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped)) + + def clean(self): + """Clean faces by removing splitter edges.""" + r = self.wrapped.removeSplitter() + # removeSplitter() returns a generic Shape type, cast to actual type of object + r = FreeCADPart.cast_to_shape(r) + return Shape.cast(r) + + def fillet(self, radius, edgeList): + """ + Fillets the specified edges of this solid. + :param radius: float > 0, the radius of the fillet + :param edgeList: a list of Edge objects, which must belong to this solid + :return: Filleted solid + """ + nativeEdges = [e.wrapped for e in edgeList] + return Shape.cast(self.wrapped.makeFillet(radius, nativeEdges)) + + def chamfer(self, length, length2, edgeList): + """ + Chamfers the specified edges of this solid. + :param length: length > 0, the length (length) of the chamfer + :param length2: length2 > 0, optional parameter for asymmetrical chamfer. Should be `None` if not required. + :param edgeList: a list of Edge objects, which must belong to this solid + :return: Chamfered solid + """ + nativeEdges = [e.wrapped for e in edgeList] + # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API + if length2: + return Shape.cast(self.wrapped.makeChamfer(length, length2, nativeEdges)) + else: + return Shape.cast(self.wrapped.makeChamfer(length, nativeEdges)) + + def shell(self, faceList, thickness, tolerance=0.0001): + """ + make a shelled solid of given by removing the list of faces + + :param faceList: list of face objects, which must be part of the solid. + :param thickness: floating point thickness. positive shells outwards, negative shells inwards + :param tolerance: modelling tolerance of the method, default=0.0001 + :return: a shelled solid + + **WARNING** The underlying FreeCAD implementation can very frequently have problems + with shelling complex geometries! + """ + nativeFaces = [f.wrapped for f in faceList] + return Shape.cast(self.wrapped.makeThickness(nativeFaces, thickness, tolerance)) + + +class Compound(Shape): + """ + a collection of disconnected solids + """ + + def __init__(self, obj): + """ + An Edge + """ + self.wrapped = obj + + # Helps identify this solid through the use of an ID + self.label = "" + + def Center(self): + return self.Center() + + @classmethod + def makeCompound(cls, listOfShapes): + """ + Create a compound out of a list of shapes + """ + solids = [s.wrapped for s in listOfShapes] + c = FreeCADPart.Compound(solids) + return Shape.cast(c) + + def fuse(self, toJoin): + return Shape.cast(self.wrapped.fuse(toJoin.wrapped)) + + def tessellate(self, tolerance): + return self.wrapped.tessellate(tolerance) + + def clean(self): + """This method is not implemented yet.""" + return self diff --git a/build/lib.linux-i686-2.7/cadquery/plugins/__init__.py b/build/lib.linux-i686-2.7/cadquery/plugins/__init__.py new file mode 100644 index 0000000..3697b9f --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/plugins/__init__.py @@ -0,0 +1,18 @@ +""" + CadQuery + Copyright (C) 2015 Parametric Products Intellectual Holdings, LLC + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" diff --git a/build/lib.linux-i686-2.7/cadquery/selectors.py b/build/lib.linux-i686-2.7/cadquery/selectors.py new file mode 100644 index 0000000..be07d7b --- /dev/null +++ b/build/lib.linux-i686-2.7/cadquery/selectors.py @@ -0,0 +1,474 @@ +""" + Copyright (C) 2011-2015 Parametric Products Intellectual Holdings, LLC + + This file is part of CadQuery. + + CadQuery is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + CadQuery is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see +""" + +import re +import math +from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound + + +class Selector(object): + """ + Filters a list of objects + + Filters must provide a single method that filters objects. + """ + def filter(self,objectList): + """ + Filter the provided list + :param objectList: list to filter + :type objectList: list of FreeCAD primatives + :return: filtered list + + The default implementation returns the original list unfiltered + + """ + return objectList + + def __and__(self, other): + return AndSelector(self, other) + + def __add__(self, other): + return SumSelector(self, other) + + def __sub__(self, other): + return SubtractSelector(self, other) + + def __neg__(self): + return InverseSelector(self) + +class NearestToPointSelector(Selector): + """ + Selects object nearest the provided point. + + If the object is a vertex or point, the distance + is used. For other kinds of shapes, the center of mass + is used to to compute which is closest. + + Applicability: All Types of Shapes + + Example:: + + CQ(aCube).vertices(NearestToPointSelector((0,1,0)) + + returns the vertex of the unit cube closest to the point x=0,y=1,z=0 + + """ + def __init__(self,pnt ): + self.pnt = pnt + def filter(self,objectList): + + def dist(tShape): + return tShape.Center().sub(Vector(*self.pnt)).Length + #if tShape.ShapeType == 'Vertex': + # return tShape.Point.sub(toVector(self.pnt)).Length + #else: + # return tShape.CenterOfMass.sub(toVector(self.pnt)).Length + + return [ min(objectList,key=dist) ] + +class BoxSelector(Selector): + """ + Selects objects inside the 3D box defined by 2 points. + + If `boundingbox` is True only the objects that have their bounding + box inside the given box is selected. Otherwise only center point + of the object is tested. + + Applicability: all types of shapes + + Example:: + + CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1)) + """ + def __init__(self, point0, point1, boundingbox=False): + self.p0 = Vector(*point0) + self.p1 = Vector(*point1) + self.test_boundingbox = boundingbox + + def filter(self, objectList): + + result = [] + x0, y0, z0 = self.p0.toTuple() + x1, y1, z1 = self.p1.toTuple() + + def isInsideBox(p): + # using XOR for checking if x/y/z is in between regardless + # of order of x/y/z0 and x/y/z1 + return ((p.x < x0) ^ (p.x < x1)) and \ + ((p.y < y0) ^ (p.y < y1)) and \ + ((p.z < z0) ^ (p.z < z1)) + + for o in objectList: + if self.test_boundingbox: + bb = o.BoundingBox() + if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \ + isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)): + result.append(o) + else: + if isInsideBox(o.Center()): + result.append(o) + + return result + +class BaseDirSelector(Selector): + """ + A selector that handles selection on the basis of a single + direction vector + """ + def __init__(self,vector,tolerance=0.0001 ): + self.direction = vector + self.TOLERANCE = tolerance + + def test(self,vec): + "Test a specified vector. Subclasses override to provide other implementations" + return True + + def filter(self,objectList): + """ + There are lots of kinds of filters, but + for planes they are always based on the normal of the plane, + and for edges on the tangent vector along the edge + """ + r = [] + for o in objectList: + #no really good way to avoid a switch here, edges and faces are simply different! + + if type(o) == Face: + # a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir + normal = o.normalAt(None) + + if self.test(normal): + r.append(o) + elif type(o) == Edge and o.geomType() == 'LINE': + #an edge is parallel to a direction if it is a line, and the line is parallel to the dir + tangent = o.tangentAt(None) + if self.test(tangent): + r.append(o) + + return r + +class ParallelDirSelector(BaseDirSelector): + """ + Selects objects parallel with the provided direction + + Applicability: + Linear Edges + Planar Faces + + Use the string syntax shortcut \|(X|Y|Z) if you want to select + based on a cardinal direction. + + Example:: + + CQ(aCube).faces(ParallelDirSelector((0,0,1)) + + selects faces with a normals in the z direction, and is equivalent to:: + + CQ(aCube).faces("|Z") + """ + + def test(self,vec): + return self.direction.cross(vec).Length < self.TOLERANCE + +class DirectionSelector(BaseDirSelector): + """ + Selects objects aligned with the provided direction + + Applicability: + Linear Edges + Planar Faces + + Use the string syntax shortcut +/-(X|Y|Z) if you want to select + based on a cardinal direction. + + Example:: + + CQ(aCube).faces(DirectionSelector((0,0,1)) + + selects faces with a normals in the z direction, and is equivalent to:: + + CQ(aCube).faces("+Z") + """ + + def test(self,vec): + return abs(self.direction.getAngle(vec) < self.TOLERANCE) + +class PerpendicularDirSelector(BaseDirSelector): + """ + Selects objects perpendicular with the provided direction + + Applicability: + Linear Edges + Planar Faces + + Use the string syntax shortcut #(X|Y|Z) if you want to select + based on a cardinal direction. + + Example:: + + CQ(aCube).faces(PerpendicularDirSelector((0,0,1)) + + selects faces with a normals perpendicular to the z direction, and is equivalent to:: + + CQ(aCube).faces("#Z") + """ + + def test(self,vec): + angle = self.direction.getAngle(vec) + r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE ) + return not r + + +class TypeSelector(Selector): + """ + Selects objects of the prescribed topological type. + + Applicability: + Faces: Plane,Cylinder,Sphere + Edges: Line,Circle,Arc + + You can use the shortcut selector %(PLANE|SPHERE|CONE) for faces, + and %(LINE|ARC|CIRCLE) for edges. + + For example this:: + + CQ(aCube).faces ( TypeSelector("PLANE") ) + + will select 6 faces, and is equivalent to:: + + CQ(aCube).faces( "%PLANE" ) + + """ + def __init__(self,typeString): + self.typeString = typeString.upper() + + def filter(self,objectList): + r = [] + for o in objectList: + if o.geomType() == self.typeString: + r.append(o) + return r + +class DirectionMinMaxSelector(Selector): + """ + Selects objects closest or farthest in the specified direction + Used for faces, points, and edges + + Applicability: + All object types. for a vertex, its point is used. for all other kinds + of objects, the center of mass of the object is used. + + You can use the string shortcuts >(X|Y|Z) or <(X|Y|Z) if you want to + select based on a cardinal direction. + + For example this:: + + CQ(aCube).faces ( DirectionMinMaxSelector((0,0,1),True ) + + Means to select the face having the center of mass farthest in the positive z direction, + and is the same as: + + CQ(aCube).faces( ">Z" ) + + Future Enhancements: + provide a nicer way to select in arbitrary directions. IE, a bit more code could + allow '>(0,0,1)' to work. + + """ + def __init__(self, vector, directionMax=True, tolerance=0.0001): + self.vector = vector + self.max = max + self.directionMax = directionMax + self.TOLERANCE = tolerance + def filter(self,objectList): + + def distance(tShape): + return tShape.Center().dot(self.vector) + #if tShape.ShapeType == 'Vertex': + # pnt = tShape.Point + #else: + # pnt = tShape.Center() + #return pnt.dot(self.vector) + + # find out the max/min distance + if self.directionMax: + d = max(map(distance, objectList)) + else: + d = min(map(distance, objectList)) + + # return all objects at the max/min distance (within a tolerance) + return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList) + +class BinarySelector(Selector): + """ + Base class for selectors that operates with two other + selectors. Subclass must implement the :filterResults(): method. + """ + def __init__(self, left, right): + self.left = left + self.right = right + + def filter(self, objectList): + return self.filterResults(self.left.filter(objectList), + self.right.filter(objectList)) + + def filterResults(self, r_left, r_right): + raise NotImplementedError + +class AndSelector(BinarySelector): + """ + Intersection selector. Returns objects that is selected by both selectors. + """ + def filterResults(self, r_left, r_right): + # return intersection of lists + return list(set(r_left) & set(r_right)) + +class SumSelector(BinarySelector): + """ + Union selector. Returns the sum of two selectors results. + """ + def filterResults(self, r_left, r_right): + # return the union (no duplicates) of lists + return list(set(r_left + r_right)) + +class SubtractSelector(BinarySelector): + """ + Difference selector. Substract results of a selector from another + selectors results. + """ + def filterResults(self, r_left, r_right): + return list(set(r_left) - set(r_right)) + +class InverseSelector(Selector): + """ + Inverts the selection of given selector. In other words, selects + all objects that is not selected by given selector. + """ + def __init__(self, selector): + self.selector = selector + + def filter(self, objectList): + # note that Selector() selects everything + return SubtractSelector(Selector(), self.selector).filter(objectList) + +class StringSyntaxSelector(Selector): + """ + Filter lists objects using a simple string syntax. All of the filters available in the string syntax + are also available ( usually with more functionality ) through the creation of full-fledged + selector objects. see :py:class:`Selector` and its subclasses + + Filtering works differently depending on the type of object list being filtered. + + :param selectorString: A two-part selector string, [selector][axis] + + :return: objects that match the specified selector + + ***Modfiers*** are ``('|','+','-','<','>','%')`` + + :\|: + parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects. + :#: + perpendicular to (same as :py:class:`PerpendicularDirSelector` ) + :+: + positive direction (same as :py:class:`DirectionSelector` ) + :-: + negative direction (same as :py:class:`DirectionSelector` ) + :>: + maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True) + :<: + minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False ) + :%: + curve/surface type (same as :py:class:`TypeSelector`) + + ***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ`` + + Selectors are a complex topic: see :ref:`selector_reference` for more information + + + + """ + def __init__(self,selectorString): + + self.axes = { + 'X': Vector(1,0,0), + 'Y': Vector(0,1,0), + 'Z': Vector(0,0,1), + 'XY': Vector(1,1,0), + 'YZ': Vector(0,1,1), + 'XZ': Vector(1,0,1) + } + + namedViews = { + 'front': ('>','Z' ), + 'back': ('<','Z'), + 'left':('<', 'X'), + 'right': ('>', 'X'), + 'top': ('>','Y'), + 'bottom': ('<','Y') + } + self.selectorString = selectorString + r = re.compile("\s*([-\+<>\|\%#])*\s*(\w+)\s*",re.IGNORECASE) + m = r.match(selectorString) + + if m != None: + if namedViews.has_key(selectorString): + (a,b) = namedViews[selectorString] + self.mySelector = self._chooseSelector(a,b ) + else: + self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1]) + else: + raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ") + + + def _chooseSelector(self,selType,selAxis): + """Sets up the underlying filters accordingly""" + + if selType == "%": + return TypeSelector(selAxis) + + #all other types need to select axis as a vector + #get the axis vector first, will throw an except if an unknown axis is used + try: + vec = self.axes[selAxis] + except KeyError: + raise ValueError ("Axis value %s not allowed: must be one of %s" % (selAxis, str(self.axes))) + + if selType in (None, "+"): + #use direction filter + return DirectionSelector(vec) + elif selType == '-': + #just use the reverse of the direction vector + return DirectionSelector(vec.multiply(-1.0)) + elif selType == "|": + return ParallelDirSelector(vec) + elif selType == ">": + return DirectionMinMaxSelector(vec,True) + elif selType == "<": + return DirectionMinMaxSelector(vec,False) + elif selType == '#': + return PerpendicularDirSelector(vec) + else: + raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ") + + def filter(self,objectList): + """ + selects minimum, maximum, positive or negative values relative to a direction + [+\|-\|<\|>\|] \ + """ + return self.mySelector.filter(objectList) diff --git a/build/lib.linux-i686-2.7/tests/TestCQGI.py b/build/lib.linux-i686-2.7/tests/TestCQGI.py new file mode 100644 index 0000000..35d8906 --- /dev/null +++ b/build/lib.linux-i686-2.7/tests/TestCQGI.py @@ -0,0 +1,170 @@ +""" + Tests CQGI functionality + + Currently, this includes: + Parsing a script, and detecting its available variables + Altering the values at runtime + defining a build_object function to return results +""" + +from cadquery import cqgi +from tests import BaseTest +import textwrap + +TESTSCRIPT = textwrap.dedent( + """ + height=2.0 + width=3.0 + (a,b) = (1.0,1.0) + foo="bar" + + result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) ) + build_object(result) + """ +) + + +class TestCQGI(BaseTest): + def test_parser(self): + model = cqgi.CQModel(TESTSCRIPT) + metadata = model.metadata + + self.assertEquals(set(metadata.parameters.keys()), {'height', 'width', 'a', 'b', 'foo'}) + + def test_build_with_empty_params(self): + model = cqgi.CQModel(TESTSCRIPT) + result = model.build() + + self.assertTrue(result.success) + self.assertTrue(len(result.results) == 1) + self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0") + + def test_build_with_different_params(self): + model = cqgi.CQModel(TESTSCRIPT) + result = model.build({'height': 3.0}) + self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0") + + def test_build_with_exception(self): + badscript = textwrap.dedent( + """ + raise ValueError("ERROR") + """ + ) + + model = cqgi.CQModel(badscript) + result = model.build({}) + self.assertFalse(result.success) + self.assertIsNotNone(result.exception) + self.assertTrue(result.exception.message == "ERROR") + + def test_that_invalid_syntax_in_script_fails_immediately(self): + badscript = textwrap.dedent( + """ + this doesnt even compile + """ + ) + + with self.assertRaises(Exception) as context: + model = cqgi.CQModel(badscript) + + self.assertTrue('invalid syntax' in context.exception) + + def test_that_two_results_are_returned(self): + script = textwrap.dedent( + """ + h = 1 + build_object(h) + h = 2 + build_object(h) + """ + ) + + model = cqgi.CQModel(script) + result = model.build({}) + self.assertEquals(2, len(result.results)) + self.assertEquals(1, result.results[0]) + self.assertEquals(2, result.results[1]) + + def test_that_assinging_number_to_string_works(self): + script = textwrap.dedent( + """ + h = "this is a string" + build_object(h) + """ + ) + result = cqgi.parse(script).build( {'h': 33.33}) + self.assertEquals(result.results[0], "33.33") + + def test_that_assigning_string_to_number_fails(self): + script = textwrap.dedent( + """ + h = 20.0 + build_object(h) + """ + ) + result = cqgi.parse(script).build( {'h': "a string"}) + self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) + + def test_that_assigning_unknown_var_fails(self): + script = textwrap.dedent( + """ + h = 20.0 + build_object(h) + """ + ) + + result = cqgi.parse(script).build( {'w': "var is not there"}) + self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) + + def test_that_not_calling_build_object_raises_error(self): + script = textwrap.dedent( + """ + h = 20.0 + """ + ) + result = cqgi.parse(script).build() + self.assertTrue(isinstance(result.exception, cqgi.NoOutputError)) + + def test_that_cq_objects_are_visible(self): + script = textwrap.dedent( + """ + r = cadquery.Workplane('XY').box(1,2,3) + build_object(r) + """ + ) + + result = cqgi.parse(script).build() + self.assertTrue(result.success) + self.assertIsNotNone(result.first_result) + + def test_setting_boolean_variable(self): + script = textwrap.dedent( + """ + h = True + build_object( "*%s*" % str(h) ) + """ + ) + + #result = cqgi.execute(script) + result = cqgi.parse(script).build({'h': False}) + + self.assertTrue(result.success) + self.assertEquals(result.first_result,'*False*') + + def test_that_only_top_level_vars_are_detected(self): + script = textwrap.dedent( + """ + h = 1.0 + w = 2.0 + + def do_stuff(): + x = 1 + y = 2 + + build_object( "result" ) + """ + ) + + model = cqgi.parse(script) + + self.assertEquals(2, len(model.metadata.parameters)) \ No newline at end of file diff --git a/build/lib.linux-i686-2.7/tests/TestCQSelectors.py b/build/lib.linux-i686-2.7/tests/TestCQSelectors.py new file mode 100644 index 0000000..f90e14b --- /dev/null +++ b/build/lib.linux-i686-2.7/tests/TestCQSelectors.py @@ -0,0 +1,358 @@ +__author__ = 'dcowden' + +""" + Tests for CadQuery Selectors + + These tests do not construct any solids, they test only selectors that query + an existing solid + +""" + +import math +import unittest,sys +import os.path + +#my modules +from tests import BaseTest,makeUnitCube,makeUnitSquareWire +from cadquery import * +from cadquery import selectors + +class TestCQSelectors(BaseTest): + + + def testWorkplaneCenter(self): + "Test Moving workplane center" + s = Workplane(Plane.XY()) + + #current point and world point should be equal + self.assertTupleAlmostEquals((0.0,0.0,0.0),s.plane.origin.toTuple(),3) + + #move origin and confirm center moves + s.center(-2.0,-2.0) + + #current point should be 0,0, but + + self.assertTupleAlmostEquals((-2.0,-2.0,0.0),s.plane.origin.toTuple(),3) + + + def testVertices(self): + t = makeUnitSquareWire() # square box + c = CQ(t) + + self.assertEqual(4,c.vertices().size() ) + self.assertEqual(4,c.edges().size() ) + self.assertEqual(0,c.vertices().edges().size() ) #no edges on any vertices + self.assertEqual(4,c.edges().vertices().size() ) #but selecting all edges still yields all vertices + self.assertEqual(1,c.wires().size()) #just one wire + self.assertEqual(0,c.faces().size()) + self.assertEqual(0,c.vertices().faces().size()) #odd combinations all work but yield no results + self.assertEqual(0,c.edges().faces().size()) + self.assertEqual(0,c.edges().vertices().faces().size()) + + def testEnd(self): + c = CQ(makeUnitSquareWire()) + self.assertEqual(4,c.vertices().size() ) #4 because there are 4 vertices + self.assertEqual(1,c.vertices().end().size() ) #1 because we started with 1 wire + + def testAll(self): + "all returns a list of CQ objects, so that you can iterate over them individually" + c = CQ(makeUnitCube()) + self.assertEqual(6,c.faces().size()) + self.assertEqual(6,len(c.faces().all())) + self.assertEqual(4,c.faces().all()[0].vertices().size() ) + + def testFirst(self): + c = CQ( makeUnitCube()) + self.assertEqual(type(c.vertices().first().val()),Vertex) + self.assertEqual(type(c.vertices().first().first().first().val()),Vertex) + + def testCompounds(self): + c = CQ(makeUnitSquareWire()) + self.assertEqual(0,c.compounds().size() ) + self.assertEqual(0,c.shells().size() ) + self.assertEqual(0,c.solids().size() ) + + def testSolid(self): + c = CQ(makeUnitCube()) + #make sure all the counts are right for a cube + self.assertEqual(1,c.solids().size() ) + self.assertEqual(6,c.faces().size() ) + self.assertEqual(12,c.edges().size()) + self.assertEqual(8,c.vertices().size() ) + self.assertEqual(0,c.compounds().size()) + + #now any particular face should result in 4 edges and four vertices + self.assertEqual(4,c.faces().first().edges().size() ) + self.assertEqual(1,c.faces().first().size() ) + self.assertEqual(4,c.faces().first().vertices().size() ) + + self.assertEqual(4,c.faces().last().edges().size() ) + + + + def testFaceTypesFilter(self): + "Filters by face type" + c = CQ(makeUnitCube()) + self.assertEqual(c.faces().size(), c.faces('%PLANE').size()) + self.assertEqual(c.faces().size(), c.faces('%plane').size()) + self.assertEqual(0, c.faces('%sphere').size()) + self.assertEqual(0, c.faces('%cone').size()) + self.assertEqual(0, c.faces('%SPHERE').size()) + + def testPerpendicularDirFilter(self): + c = CQ(makeUnitCube()) + + self.assertEqual(8,c.edges("#Z").size() ) #8 edges are perp. to z + self.assertEqual(4, c.faces("#Z").size()) #4 faces are perp to z too! + + def testFaceDirFilter(self): + c = CQ(makeUnitCube()) + #a cube has one face in each direction + self.assertEqual(1, c.faces("+Z").size()) + self.assertEqual(1, c.faces("-Z").size()) + self.assertEqual(1, c.faces("+X").size()) + self.assertEqual(1, c.faces("X").size()) #should be same as +X + self.assertEqual(1, c.faces("-X").size()) + self.assertEqual(1, c.faces("+Y").size()) + self.assertEqual(1, c.faces("-Y").size()) + self.assertEqual(0, c.faces("XY").size()) + + def testParallelPlaneFaceFilter(self): + c = CQ(makeUnitCube()) + + #faces parallel to Z axis + self.assertEqual(2, c.faces("|Z").size()) + #TODO: provide short names for ParallelDirSelector + self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,1)))).size()) #same thing as above + self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,-1)))).size()) #same thing as above + + #just for fun, vertices on faces parallel to z + self.assertEqual(8, c.faces("|Z").vertices().size()) + + def testParallelEdgeFilter(self): + c = CQ(makeUnitCube()) + self.assertEqual(4, c.edges("|Z").size()) + self.assertEqual(4, c.edges("|X").size()) + self.assertEqual(4, c.edges("|Y").size()) + + def testMaxDistance(self): + c = CQ(makeUnitCube()) + + #should select the topmost face + self.assertEqual(1, c.faces(">Z").size()) + self.assertEqual(4, c.faces(">Z").vertices().size()) + + #vertices should all be at z=1, if this is the top face + self.assertEqual(4, len(c.faces(">Z").vertices().vals() )) + for v in c.faces(">Z").vertices().vals(): + self.assertAlmostEqual(1.0,v.Z,3) + + # test the case of multiple objects at the same distance + el = c.edges(" 0,0,1 + e = c.edges(selectors.NearestToPointSelector(t)).vals()[0] + v = c.edges(selectors.NearestToPointSelector(t)).vertices().vals() + self.assertEqual(2,len(v)) + + #nearest solid is myself + s = c.solids(selectors.NearestToPointSelector(t)).vals() + self.assertEqual(1,len(s)) + + def testBox(self): + c = CQ(makeUnitCube()) + + # test vertice selection + test_data_vertices = [ + # box point0, box point1, selected vertice + ((0.9, 0.9, 0.9), (1.1, 1.1, 1.1), (1.0, 1.0, 1.0)), + ((-0.1, 0.9, 0.9), (0.9, 1.1, 1.1), (0.0, 1.0, 1.0)), + ((-0.1, -0.1, 0.9), (0.1, 0.1, 1.1), (0.0, 0.0, 1.0)), + ((-0.1, -0.1, -0.1), (0.1, 0.1, 0.1), (0.0, 0.0, 0.0)), + ((0.9, -0.1, -0.1), (1.1, 0.1, 0.1), (1.0, 0.0, 0.0)), + ((0.9, 0.9, -0.1), (1.1, 1.1, 0.1), (1.0, 1.0, 0.0)), + ((-0.1, 0.9, -0.1), (0.1, 1.1, 0.1), (0.0, 1.0, 0.0)), + ((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0)) + ] + + for d in test_data_vertices: + vl = c.vertices(selectors.BoxSelector(d[0], d[1])).vals() + self.assertEqual(1, len(vl)) + v = vl[0] + self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3) + + # this time box points are swapped + vl = c.vertices(selectors.BoxSelector(d[1], d[0])).vals() + self.assertEqual(1, len(vl)) + v = vl[0] + self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3) + + # test multiple vertices selection + vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, 0.9),(0.1, 1.1, 1.1))).vals() + self.assertEqual(2, len(vl)) + vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, -0.1),(0.1, 1.1, 1.1))).vals() + self.assertEqual(4, len(vl)) + + # test edge selection + test_data_edges = [ + # box point0, box point1, edge center + ((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)), + ((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)), + ((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)), + ((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0)) + ] + + for d in test_data_edges: + el = c.edges(selectors.BoxSelector(d[0], d[1])).vals() + self.assertEqual(1, len(el)) + ec = el[0].Center() + self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3) + + # test again by swapping box points + el = c.edges(selectors.BoxSelector(d[1], d[0])).vals() + self.assertEqual(1, len(el)) + ec = el[0].Center() + self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3) + + # test multiple edge selection + el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals() + self.assertEqual(2, len(el)) + el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals() + self.assertEqual(3, len(el)) + + # test face selection + test_data_faces = [ + # box point0, box point1, face center + ((0.4, -0.1, 0.4), (0.6, 0.1, 0.6), (0.5, 0.0, 0.5)), + ((0.9, 0.4, 0.4), (1.1, 0.6, 0.6), (1.0, 0.5, 0.5)), + ((0.4, 0.4, 0.9), (0.6, 0.6, 1.1), (0.5, 0.5, 1.0)), + ((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0)) + ] + + for d in test_data_faces: + fl = c.faces(selectors.BoxSelector(d[0], d[1])).vals() + self.assertEqual(1, len(fl)) + fc = fl[0].Center() + self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3) + + # test again by swapping box points + fl = c.faces(selectors.BoxSelector(d[1], d[0])).vals() + self.assertEqual(1, len(fl)) + fc = fl[0].Center() + self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3) + + # test multiple face selection + fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals() + self.assertEqual(2, len(fl)) + fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals() + self.assertEqual(3, len(fl)) + + # test boundingbox option + el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals() + self.assertEqual(1, len(el)) + fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals() + self.assertEqual(0, len(fl)) + fl = c.faces(selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals() + self.assertEqual(1, len(fl)) + + def testAndSelector(self): + c = CQ(makeUnitCube()) + + S = selectors.StringSyntaxSelector + BS = selectors.BoxSelector + + el = c.edges(selectors.AndSelector(S('|X'), BS((-2,-2,0.1), (2,2,2)))).vals() + self.assertEqual(2, len(el)) + + # test 'and' (intersection) operator + el = c.edges(S('|X') & BS((-2,-2,0.1), (2,2,2))).vals() + self.assertEqual(2, len(el)) + + def testSumSelector(self): + c = CQ(makeUnitCube()) + + S = selectors.StringSyntaxSelector + + fl = c.faces(selectors.SumSelector(S(">Z"), S("Z") + S("X"))).vals() + self.assertEqual(3, len(fl)) + + # test the subtract operator + fl = c.faces(S("#Z") - S(">X")).vals() + self.assertEqual(3, len(fl)) + + def testInverseSelector(self): + c = CQ(makeUnitCube()) + + S = selectors.StringSyntaxSelector + + fl = c.faces(selectors.InverseSelector(S('>Z'))).vals() + self.assertEqual(5, len(fl)) + el = c.faces('>Z').edges(selectors.InverseSelector(S('>X'))).vals() + self.assertEqual(3, len(el)) + + # test invert operator + fl = c.faces(-S('>Z')).vals() + self.assertEqual(5, len(fl)) + el = c.faces('>Z').edges(-S('>X')).vals() + self.assertEqual(3, len(el)) + + def testFaceCount(self): + c = CQ(makeUnitCube()) + self.assertEqual( 6, c.faces().size() ) + self.assertEqual( 2, c.faces("|Z").size() ) + + def testVertexFilter(self): + "test selecting vertices on a face" + c = CQ(makeUnitCube()) + + #TODO: filters work ok, but they are in global coordinates which sux. it would be nice + #if they were available in coordinates local to the selected face + + v2 = c.faces("+Z").vertices(" + + + + + + +""" + +TEST_RESULT_TEMPLATE=""" +

%(name)s

+ %(svg)s +
+ +""" + +#clean up any summary file that is in the output directory. +#i know, this sux, but there is no other way to do this in 2.6, as we cannot do class fixutres till 2.7 +writeStringToFile(SUMMARY_TEMPLATE,SUMMARY_FILE) + + +class TestCadQuery(BaseTest): + + def tearDown(self): + """ + Update summary with data from this test. + This is a really hackey way of doing it-- we get a startup event from module load, + but there is no way in unittest to get a single shutdown event-- except for stuff in 2.7 and above + + So what we do here is to read the existing file, stick in more content, and leave it + """ + svgFile = os.path.join(OUTDIR,self._testMethodName + ".svg") + + #all tests do not produce output + if os.path.exists(svgFile): + existingSummary = readFileAsString(SUMMARY_FILE) + svgText = readFileAsString(svgFile) + svgText = svgText.replace('',"") + + #now write data into the file + #the content we are replacing it with also includes the marker, so it can be replaced again + existingSummary = existingSummary.replace("", TEST_RESULT_TEMPLATE % ( + dict(svg=svgText, name=self._testMethodName))) + + writeStringToFile(existingSummary,SUMMARY_FILE) + + def saveModel(self, shape): + """ + shape must be a CQ object + Save models in SVG and STEP format + """ + shape.exportSvg(os.path.join(OUTDIR,self._testMethodName + ".svg")) + shape.val().exportStep(os.path.join(OUTDIR,self._testMethodName + ".step")) + + def testToFreeCAD(self): + """ + Tests to make sure that a CadQuery object is converted correctly to a FreeCAD object. + """ + r = Workplane('XY').rect(5, 5).extrude(5) + + r = r.toFreecad() + + self.assertEqual(12, len(r.Edges)) + + def testToSVG(self): + """ + Tests to make sure that a CadQuery object is converted correctly to SVG + """ + r = Workplane('XY').rect(5, 5).extrude(5) + + r_str = r.toSvg() + + # Make sure that a couple of sections from the SVG output make sense + self.assertTrue(r_str.index('path d=" M 2.35965 -2.27987 L 4.0114 -3.23936 "') > 0) + self.assertTrue(r_str.index('line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3"') > 0) + + def testCubePlugin(self): + """ + Tests a plugin that combines cubes together with a base + :return: + """ + #make the plugin method + def makeCubes(self,length): + #self refers to the CQ or Workplane object + + #inner method that creates a cube + def _singleCube(pnt): + #pnt is a location in local coordinates + #since we're using eachpoint with useLocalCoordinates=True + return Solid.makeBox(length,length,length,pnt) + + #use CQ utility method to iterate over the stack, call our + #method, and convert to/from local coordinates. + return self.eachpoint(_singleCube,True) + + #link the plugin in + Workplane.makeCubes = makeCubes + + #call it + result = Workplane("XY").box(6.0,8.0,0.5).faces(">Z").rect(4.0,4.0,forConstruction=True).vertices() + result = result.makeCubes(1.0) + result = result.combineSolids() + self.saveModel(result) + self.assertEquals(1,result.solids().size() ) + + + def testCylinderPlugin(self): + """ + Tests a cylinder plugin. + The plugin creates cylinders of the specified radius and height for each item on the stack + + This is a very short plugin that illustrates just about the simplest possible + plugin + """ + + def cylinders(self,radius,height): + + def _cyl(pnt): + #inner function to build a cylinder + return Solid.makeCylinder(radius,height,pnt) + + #combine all the cylinders into a single compound + r = self.eachpoint(_cyl,True).combineSolids() + return r + Workplane.cyl = cylinders + + #now test. here we want weird workplane to see if the objects are transformed right + s = Workplane(Plane(Vector((0,0,0)),Vector((1,-1,0)),Vector((1,1,0)))).rect(2.0,3.0,forConstruction=True).vertices() \ + .cyl(0.25,0.5) + self.assertEquals(1,s.solids().size() ) + self.saveModel(s) + + def testPolygonPlugin(self): + """ + Tests a plugin to make regular polygons around points on the stack + + Demonstratings using eachpoint to allow working in local coordinates + to create geometry + """ + def rPoly(self,nSides,diameter): + + 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) + + Workplane.rPoly = rPoly + + s = Workplane("XY").box(4.0,4.0,0.25).faces(">Z").workplane().rect(2.0,2.0,forConstruction=True).vertices()\ + .rPoly(5,0.5).cutThruAll() + + self.assertEquals(26,s.faces().size()) #6 base sides, 4 pentagons, 5 sides each = 26 + self.saveModel(s) + + + def testPointList(self): + """ + Tests adding points and using them + """ + c = CQ(makeUnitCube()) + + s = c.faces(">Z").workplane().pushPoints([(-0.3, 0.3), (0.3, 0.3), (0, 0)]) + self.assertEqual(3, s.size()) + #TODO: is the ability to iterate over points with circle really worth it? + #maybe we should just require using all() and a loop for this. the semantics and + #possible combinations got too hard ( ie, .circle().circle() ) was really odd + body = s.circle(0.05).cutThruAll() + self.saveModel(body) + self.assertEqual(9, body.faces().size()) + + # Test the case when using eachpoint with only a blank workplane + def callback_fn(pnt): + self.assertEqual((0.0, 0.0), (pnt.x, pnt.y)) + + r = Workplane('XY') + r.objects = [] + r.eachpoint(callback_fn) + + + def testWorkplaneFromFace(self): + s = CQ(makeUnitCube()).faces(">Z").workplane() #make a workplane on the top face + r = s.circle(0.125).cutBlind(-2.0) + self.saveModel(r) + #the result should have 7 faces + self.assertEqual(7,r.faces().size() ) + self.assertEqual(type(r.val()), Solid) + self.assertEqual(type(r.first().val()),Solid) + + def testFrontReference(self): + s = CQ(makeUnitCube()).faces("front").workplane() #make a workplane on the top face + r = s.circle(0.125).cutBlind(-2.0) + self.saveModel(r) + #the result should have 7 faces + self.assertEqual(7,r.faces().size() ) + self.assertEqual(type(r.val()), Solid) + self.assertEqual(type(r.first().val()),Solid) + + def testRotate(self): + """Test solid rotation at the CQ object level.""" + box = Workplane("XY").box(1, 1, 5) + box.rotate((0, 0, 0), (1, 0, 0), 90) + startPoint = box.faces("Z").circle(1.5)\ + .workplane(offset=3.0).rect(0.75,0.5).loft(combine=True) + self.saveModel(s) + #self.assertEqual(1,s.solids().size() ) + #self.assertEqual(8,s.faces().size() ) + + def testRevolveCylinder(self): + """ + Test creating a solid using the revolve operation. + :return: + """ + # The dimensions of the model. These can be modified rather than changing the + # shape's code directly. + rectangle_width = 10.0 + rectangle_length = 10.0 + angle_degrees = 360.0 + + #Test revolve without any options for making a cylinder + result = Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + + #Test revolve when only setting the angle to revolve through + result = Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(270.0) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + #Test when passing revolve the angle and the axis of revolution's start point + result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve(270.0,(-5,-5)) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + #Test when passing revolve the angle and both the start and ends of the axis of revolution + result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve(270.0,(-5, -5),(-5, 5)) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + #Testing all of the above without combine + result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) + self.assertEqual(3, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve(270.0,(-5,-5),(-5,5), False) + self.assertEqual(5, result.faces().size()) + self.assertEqual(6, result.vertices().size()) + self.assertEqual(9, result.edges().size()) + + def testRevolveDonut(self): + """ + Test creating a solid donut shape with square walls + :return: + """ + # The dimensions of the model. These can be modified rather than changing the + # shape's code directly. + rectangle_width = 10.0 + rectangle_length = 10.0 + angle_degrees = 360.0 + + result = Workplane("XY").rect(rectangle_width, rectangle_length, True)\ + .revolve(angle_degrees, (20, 0), (20, 10)) + self.assertEqual(4, result.faces().size()) + self.assertEqual(4, result.vertices().size()) + self.assertEqual(6, result.edges().size()) + + def testRevolveCone(self): + """ + Test creating a solid from a revolved triangle + :return: + """ + result = Workplane("XY").lineTo(0,10).lineTo(5,0).close().revolve() + self.assertEqual(2, result.faces().size()) + self.assertEqual(2, result.vertices().size()) + self.assertEqual(3, result.edges().size()) + + def testTwistExtrude(self): + """ + Tests extrusion while twisting through an angle. + """ + profile = Workplane('XY').rect(10, 10) + r = profile.twistExtrude(10, 45, False) + + self.assertEqual(6, r.faces().size()) + + def testTwistExtrudeCombine(self): + """ + Tests extrusion while twisting through an angle, combining with other solids. + """ + profile = Workplane('XY').rect(10, 10) + r = profile.twistExtrude(10, 45) + + self.assertEqual(6, r.faces().size()) + + def testRectArray(self): + NUMX=3 + NUMY=3 + s = Workplane("XY").box(40,40,5,centered=(True,True,True)).faces(">Z").workplane().rarray(8.0,8.0,NUMX,NUMY,True).circle(2.0).extrude(2.0) + #s = Workplane("XY").box(40,40,5,centered=(True,True,True)).faces(">Z").workplane().circle(2.0).extrude(2.0) + self.saveModel(s) + self.assertEqual(6+NUMX*NUMY*2,s.faces().size()) #6 faces for the box, 2 faces for each cylinder + + def testNestedCircle(self): + s = Workplane("XY").box(40,40,5).pushPoints([(10,0),(0,10)]).circle(4).circle(2).extrude(4) + self.saveModel(s) + self.assertEqual(14,s.faces().size() ) + + def testLegoBrick(self): + #test making a simple lego brick + #which of the below + + #inputs + lbumps = 8 + wbumps = 2 + + #lego brick constants + P = 8.0 #nominal pitch + c = 0.1 #clearance on each brick side + H = 1.2 * P #nominal height of a brick + bumpDiam = 4.8 #the standard bump diameter + t = ( P - ( 2*c) - bumpDiam ) / 2.0 # the nominal thickness of the walls, normally 1.5 + + postDiam = P - t #works out to 6.5 + total_length = lbumps*P - 2.0*c + total_width = wbumps*P - 2.0*c + + #build the brick + s = Workplane("XY").box(total_length,total_width,H) #make the base + s = s.faces("Z").workplane().rarray(P,P,lbumps,wbumps,True).circle(bumpDiam/2.0).extrude(1.8) # make the bumps on the top + + #add posts on the bottom. posts are different diameter depending on geometry + #solid studs for 1 bump, tubes for multiple, none for 1x1 + tmp = s.faces(" 1 and wbumps > 1: + tmp = tmp.rarray(P,P,lbumps - 1,wbumps - 1,center=True).circle(postDiam/2.0).circle(bumpDiam/2.0).extrude(H-t) + elif lbumps > 1: + tmp = tmp.rarray(P,P,lbumps - 1,1,center=True).circle(t).extrude(H-t) + elif wbumps > 1: + tmp = tmp.rarray(P,P,1,wbumps -1,center=True).circle(t).extrude(H-t) + + self.saveModel(s) + + def testAngledHoles(self): + s = Workplane("front").box(4.0,4.0,0.25).faces(">Z").workplane().transformed(offset=Vector(0,-1.5,1.0),rotate=Vector(60,0,0))\ + .rect(1.5,1.5,forConstruction=True).vertices().hole(0.25) + self.saveModel(s) + self.assertEqual(10,s.faces().size()) + + def testTranslateSolid(self): + c = CQ(makeUnitCube()) + self.assertAlmostEqual(0.0,c.faces("Z').workplane().circle(0.125).extrude(0.5,True) #make a boss, not updating the original + self.assertEqual(8,r.faces().size()) #just the boss faces + self.assertEqual(8,c.faces().size()) #original is modified too + + def testSolidReferencesCombineTrue(self): + s = Workplane(Plane.XY()) + r = s.rect(2.0,2.0).extrude(0.5) + self.assertEqual(6,r.faces().size() ) #the result of course has 6 faces + self.assertEqual(0,s.faces().size() ) # the original workplane does not, because it did not have a solid initially + + t = r.faces(">Z").workplane().rect(0.25,0.25).extrude(0.5,True) + self.assertEqual(11,t.faces().size()) #of course the result has 11 faces + self.assertEqual(11,r.faces().size()) #r does as well. the context solid for r was updated since combine was true + self.saveModel(r) + + def testSolidReferenceCombineFalse(self): + s = Workplane(Plane.XY()) + r = s.rect(2.0,2.0).extrude(0.5) + self.assertEqual(6,r.faces().size() ) #the result of course has 6 faces + self.assertEqual(0,s.faces().size() ) # the original workplane does not, because it did not have a solid initially + + t = r.faces(">Z").workplane().rect(0.25,0.25).extrude(0.5,False) + self.assertEqual(6,t.faces().size()) #result has 6 faces, becuase it was not combined with the original + self.assertEqual(6,r.faces().size()) #original is unmodified as well + #subseuent opertions use that context solid afterwards + + def testSimpleWorkplane(self): + """ + A simple square part with a hole in it + """ + s = Workplane(Plane.XY()) + r = s.rect(2.0,2.0).extrude(0.5)\ + .faces(">Z").workplane()\ + .circle(0.25).cutBlind(-1.0) + + self.saveModel(r) + self.assertEqual(7,r.faces().size() ) + + def testMultiFaceWorkplane(self): + """ + Test Creation of workplane from multiple co-planar face + selection. + """ + s = Workplane('XY').box(1,1,1).faces('>Z').rect(1,0.5).cutBlind(-0.2) + + w = s.faces('>Z').workplane() + o = w.objects[0] # origin of the workplane + self.assertAlmostEqual(o.x, 0., 3) + self.assertAlmostEqual(o.y, 0., 3) + self.assertAlmostEqual(o.z, 0.5, 3) + + def testTriangularPrism(self): + s = Workplane("XY").lineTo(1,0).lineTo(1,1).close().extrude(0.2) + self.saveModel(s) + + def testMultiWireWorkplane(self): + """ + A simple square part with a hole in it-- but this time done as a single extrusion + with two wires, as opposed to s cut + """ + s = Workplane(Plane.XY()) + r = s.rect(2.0,2.0).circle(0.25).extrude(0.5) + + self.saveModel(r) + self.assertEqual(7,r.faces().size() ) + + def testConstructionWire(self): + """ + Tests a wire with several holes, that are based on the vertices of a square + also tests using a workplane plane other than XY + """ + s = Workplane(Plane.YZ()) + r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices().circle(0.125).extrude(0.5) + self.saveModel(r) + self.assertEqual(10,r.faces().size() ) # 10 faces-- 6 plus 4 holes, the vertices of the second rect. + + def testTwoWorkplanes(self): + """ + Tests a model that uses more than one workplane + """ + #base block + s = Workplane(Plane.XY()) + + #TODO: this syntax is nice, but the iteration might not be worth + #the complexity. + #the simpler and slightly longer version would be: + # r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices() + # for c in r.all(): + # c.circle(0.125).extrude(0.5,True) + r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices().circle(0.125).extrude(0.5) + + #side hole, blind deep 1.9 + t = r.faces(">Y").workplane().circle(0.125).cutBlind(-1.9) + self.saveModel(t) + self.assertEqual(12,t.faces().size() ) + + def testCut(self): + """ + Tests the cut function by itself to catch the case where a Solid object is passed. + """ + s = Workplane(Plane.XY()) + currentS = s.rect(2.0,2.0).extrude(0.5) + toCut = s.rect(1.0,1.0).extrude(0.5) + + currentS.cut(toCut.val()) + + self.assertEqual(10,currentS.faces().size()) + + def testCutThroughAll(self): + """ + Tests a model that uses more than one workplane + """ + #base block + s = Workplane(Plane.XY()) + r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices().circle(0.125).extrude(0.5) + + #side hole, thru all + t = r.faces(">Y").workplane().circle(0.125).cutThruAll() + self.saveModel(t) + self.assertEqual(11,t.faces().size() ) + + def testCutToFaceOffsetNOTIMPLEMENTEDYET(self): + """ + Tests cutting up to a given face, or an offset from a face + """ + #base block + s = Workplane(Plane.XY()) + r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices().circle(0.125).extrude(0.5) + + #side hole, up to 0.1 from the last face + try: + t = r.faces(">Y").workplane().circle(0.125).cutToOffsetFromFace(r.faces().mminDist(Dir.Y),0.1) + self.assertEqual(10,t.faces().size() ) #should end up being a blind hole + t.first().val().exportStep('c:/temp/testCutToFace.STEP') + except: + pass + #Not Implemented Yet + + def testWorkplaneOnExistingSolid(self): + "Tests extruding on an existing solid" + c = CQ( makeUnitCube()).faces(">Z").workplane().circle(0.25).circle(0.125).extrude(0.25) + self.saveModel(c) + self.assertEqual(10,c.faces().size() ) + + + def testWorkplaneCenterMove(self): + #this workplane is centered at x=0.5,y=0.5, the center of the upper face + s = Workplane("XY").box(1,1,1).faces(">Z").workplane().center(-0.5,-0.5) # move the center to the corner + + t = s.circle(0.25).extrude(0.2) # make a boss + self.assertEqual(9,t.faces().size() ) + self.saveModel(t) + + + def testBasicLines(self): + "Make a triangluar boss" + global OUTDIR + s = Workplane(Plane.XY()) + + #TODO: extrude() should imply wire() if not done already + #most users dont understand what a wire is, they are just drawing + + r = s.lineTo(1.0,0).lineTo(0,1.0).close().wire().extrude(0.25) + r.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesStep1.STEP')) + + self.assertEqual(0,s.faces().size()) #no faces on the original workplane + self.assertEqual(5,r.faces().size() ) # 5 faces on newly created object + + #now add a circle through a side face + r.faces("+XY").workplane().circle(0.08).cutThruAll() + self.assertEqual(6,r.faces().size()) + r.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesXY.STEP')) + + #now add a circle through a top + r.faces("+Z").workplane().circle(0.08).cutThruAll() + self.assertEqual(9,r.faces().size()) + r.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesZ.STEP')) + + self.saveModel(r) + + def test2DDrawing(self): + """ + Draw things like 2D lines and arcs, should be expanded later to include all 2D constructs + """ + s = Workplane(Plane.XY()) + r = s.lineTo(1.0, 0.0) \ + .lineTo(1.0, 1.0) \ + .threePointArc((1.0, 1.5), (0.0, 1.0)) \ + .lineTo(0.0, 0.0) \ + .moveTo(1.0, 0.0) \ + .lineTo(2.0, 0.0) \ + .lineTo(2.0, 2.0) \ + .threePointArc((2.0, 2.5), (0.0, 2.0)) \ + .lineTo(-2.0, 2.0) \ + .lineTo(-2.0, 0.0) \ + .close() + + self.assertEqual(1, r.wires().size()) + + # Test the *LineTo functions + s = Workplane(Plane.XY()) + r = s.hLineTo(1.0).vLineTo(1.0).hLineTo(0.0).close() + + self.assertEqual(1, r.wire().size()) + self.assertEqual(4, r.edges().size()) + + # Test the *Line functions + s = Workplane(Plane.XY()) + r = s.hLine(1.0).vLine(1.0).hLine(-1.0).close() + + self.assertEqual(1, r.wire().size()) + self.assertEqual(4, r.edges().size()) + + # Test the move function + s = Workplane(Plane.XY()) + r = s.move(1.0, 1.0).hLine(1.0).vLine(1.0).hLine(-1.0).close() + + self.assertEqual(1, r.wire().size()) + self.assertEqual(4, r.edges().size()) + self.assertEqual((1.0, 1.0), + (r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0)))\ + .first().val().X, + r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0)))\ + .first().val().Y)) + + def testLargestDimension(self): + """ + Tests the largestDimension function when no solids are on the stack and when there are + """ + r = Workplane('XY').box(1, 1, 1) + dim = r.largestDimension() + + self.assertAlmostEqual(8.66025403784, dim) + + r = Workplane('XY') + dim = r.largestDimension() + + self.assertEqual(-1, dim) + + def testOccBottle(self): + """ + Make the OCC bottle example. + """ + + L = 20.0 + w = 6.0 + t = 3.0 + + s = Workplane(Plane.XY()) + #draw half the profile of the bottle + p = s.center(-L/2.0,0).vLine(w/2.0).threePointArc((L/2.0, w/2.0 + t),(L,w/2.0)).vLine(-w/2.0).mirrorX()\ + .extrude(30.0,True) + + #make the neck + p.faces(">Z").workplane().circle(3.0).extrude(2.0,True) #.edges().fillet(0.05) + + #make a shell + p.faces(">Z").shell(0.3) + self.saveModel(p) + + + def testSplineShape(self): + """ + Tests making a shape with an edge that is a spline + """ + 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) + self.saveModel(r) + + def testSimpleMirror(self): + """ + Tests a simple mirroring operation + """ + s = Workplane("XY").lineTo(2, 2).threePointArc((3, 1), (2, 0)) \ + .mirrorX().extrude(0.25) + self.assertEquals(6, s.faces().size()) + self.saveModel(s) + + def testUnorderedMirror(self): + """ + Tests whether or not a wire can be mirrored if its mirror won't connect to it + """ + r = 20 + s = 7 + t = 1.5 + + points = [ + (0, t/2), + (r/2-1.5*t, r/2-t), + (s/2, r/2-t), + (s/2, r/2), + (r/2, r/2), + (r/2, s/2), + (r/2-t, s/2), + (r/2-t, r/2-1.5*t), + (t/2, 0) + ] + + r = Workplane("XY").polyline(points).mirrorX() + + self.assertEquals(1, r.wires().size()) + self.assertEquals(18, r.edges().size()) + + # def testChainedMirror(self): + # """ + # Tests whether or not calling mirrorX().mirrorY() works correctly + # """ + # r = 20 + # s = 7 + # t = 1.5 + # + # points = [ + # (0, t/2), + # (r/2-1.5*t, r/2-t), + # (s/2, r/2-t), + # (s/2, r/2), + # (r/2, r/2), + # (r/2, s/2), + # (r/2-t, s/2), + # (r/2-t, r/2-1.5*t), + # (t/2, 0) + # ] + # + # r = Workplane("XY").polyline(points).mirrorX().mirrorY() + # + # self.assertEquals(1, r.wires().size()) + # self.assertEquals(32, r.edges().size()) + + #TODO: Re-work testIbeam test below now that chaining works + #TODO: Add toLocalCoords and toWorldCoords tests + + def testIbeam(self): + """ + Make an ibeam. demonstrates fancy mirroring + """ + s = Workplane(Plane.XY()) + L = 100.0 + H = 20.0 + W = 20.0 + + t = 1.0 + #TODO: for some reason doing 1/4 of the profile and mirroring twice ( .mirrorX().mirrorY() ) + #did not work, due to a bug in freecad-- it was losing edges when creating a composite wire. + #i just side-stepped it for now + + pts = [ + (0, H/2.0), + (W/2.0, H/2.0), + (W/2.0, (H/2.0 - t)), + (t/2.0, (H/2.0-t)), + (t/2.0, (t - H/2.0)), + (W/2.0, (t - H/2.0)), + (W/2.0, H / -2.0), + (0, H/-2.0) + ] + r = s.polyline(pts).mirrorY() #these other forms also work + res = r.extrude(L) + self.saveModel(res) + + def testCone(self): + """ + Tests that a simple cone works + """ + s = Solid.makeCone(0, 1.0, 2.0) + t = CQ(s) + self.saveModel(t) + self.assertEqual(2, t.faces().size()) + + def testFillet(self): + """ + Tests filleting edges on a solid + """ + c = CQ( makeUnitCube()).faces(">Z").workplane().circle(0.25).extrude(0.25,True).edges("|Z").fillet(0.2) + self.saveModel(c) + self.assertEqual(12,c.faces().size() ) + + def testChamfer(self): + """ + Test chamfer API with a box shape + """ + cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1) + self.saveModel(cube) + self.assertEqual(10, cube.faces().size()) + + def testChamferAsymmetrical(self): + """ + Test chamfer API with a box shape for asymmetrical lengths + """ + cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1, 0.2) + self.saveModel(cube) + self.assertEqual(10, cube.faces().size()) + + # test if edge lengths are different + edge = cube.edges(">Z").vals()[0] + self.assertAlmostEqual(0.6, edge.Length(), 3) + edge = cube.edges("|Z").vals()[0] + self.assertAlmostEqual(0.9, edge.Length(), 3) + + def testChamferCylinder(self): + """ + Test chamfer API with a cylinder shape + """ + cylinder = Workplane("XY").circle(1).extrude(1).faces(">Z").chamfer(0.1) + self.saveModel(cylinder) + self.assertEqual(4, cylinder.faces().size()) + + def testCounterBores(self): + """ + Tests making a set of counterbored holes in a face + """ + c = CQ(makeCube(3.0)) + pnts = [ + (-1.0, -1.0), (0.0, 0.0), (1.0, 1.0) + ] + c.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25, 0.75) + self.assertEquals(18, c.faces().size()) + self.saveModel(c) + + # Tests the case where the depth of the cboreHole is not specified + c2 = CQ(makeCube(3.0)) + pnts = [ + (-1.0, -1.0), (0.0, 0.0), (1.0, 1.0) + ] + c2.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25) + self.assertEquals(15, c2.faces().size()) + + def testCounterSinks(self): + """ + Tests countersinks + """ + s = Workplane(Plane.XY()) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + self.saveModel(result) + + def testSplitKeepingHalf(self): + """ + Tests splitting a solid + """ + + #drill a hole in the side + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() + + self.assertEqual(7, c.faces().size()) + + #now cut it in half sideways + c.faces(">Y").workplane(-0.5).split(keepTop=True) + self.saveModel(c) + self.assertEqual(8, c.faces().size()) + + def testSplitKeepingBoth(self): + """ + Tests splitting a solid + """ + + #drill a hole in the side + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() + self.assertEqual(7, c.faces().size()) + + #now cut it in half sideways + result = c.faces(">Y").workplane(-0.5).split(keepTop=True, keepBottom=True) + + #stack will have both halves, original will be unchanged + self.assertEqual(2, result.solids().size()) # two solids are on the stack, eac + self.assertEqual(8, result.solids().item(0).faces().size()) + self.assertEqual(8, result.solids().item(1).faces().size()) + + def testSplitKeepingBottom(self): + """ + Tests splitting a solid improperly + """ + # Drill a hole in the side + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() + self.assertEqual(7, c.faces().size()) + + # Now cut it in half sideways + result = c.faces(">Y").workplane(-0.5).split(keepTop=False, keepBottom=True) + + #stack will have both halves, original will be unchanged + self.assertEqual(1, result.solids().size()) # one solid is on the stack + self.assertEqual(8, result.solids().item(0).faces().size()) + + def testBoxDefaults(self): + """ + Tests creating a single box + """ + s = Workplane("XY").box(2, 3, 4) + self.assertEquals(1, s.solids().size()) + self.saveModel(s) + + def testSimpleShell(self): + """ + Create s simple box + """ + s = Workplane("XY").box(2, 2, 2).faces("+Z").shell(0.05) + self.saveModel(s) + self.assertEquals(23, s.faces().size()) + + + def testOpenCornerShell(self): + s = Workplane("XY").box(1, 1, 1) + s1 = s.faces("+Z") + s1.add(s.faces("+Y")).add(s.faces("+X")) + self.saveModel(s1.shell(0.2)) + + # Tests the list option variation of add + s1 = s.faces("+Z") + s1.add(s.faces("+Y")).add([s.faces("+X")]) + + # Tests the raw object option variation of add + s1 = s.faces("+Z") + s1.add(s.faces("+Y")).add(s.faces("+X").val().wrapped) + + def testTopFaceFillet(self): + s = Workplane("XY").box(1, 1, 1).faces("+Z").edges().fillet(0.1) + self.assertEquals(s.faces().size(), 10) + self.saveModel(s) + + def testBoxPointList(self): + """ + Tests creating an array of boxes + """ + s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=True) + #1 object, 4 solids because the object is a compound + self.assertEquals(1, s.solids().size()) + self.assertEquals(1, s.size()) + self.saveModel(s) + + s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=False) + #4 objects, 4 solids, because each is a separate solid + self.assertEquals(4, s.size()) + self.assertEquals(4, s.solids().size()) + + def testBoxCombine(self): + s = Workplane("XY").box(4, 4, 0.5).faces(">Z").workplane().rect(3, 3, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=True) + + self.saveModel(s) + self.assertEquals(1, s.solids().size()) # we should have one big solid + self.assertEquals(26, s.faces().size()) # should have 26 faces. 6 for the box, and 4x5 for the smaller cubes + + def testSphereDefaults(self): + s = Workplane("XY").sphere(10) + #self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.assertEquals(1, s.solids().size()) + self.assertEquals(1, s.faces().size()) + + def testSphereCustom(self): + s = Workplane("XY").sphere(10, angle1=0, angle2=90, angle3=360, centered=(False, False, False)) + self.saveModel(s) + self.assertEquals(1, s.solids().size()) + self.assertEquals(2, s.faces().size()) + + def testSpherePointList(self): + s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().sphere(0.25, combine=False) + #self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.assertEquals(4, s.solids().size()) + self.assertEquals(4, s.faces().size()) + + def testSphereCombine(self): + s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().sphere(0.25, combine=True) + #self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.assertEquals(1, s.solids().size()) + self.assertEquals(4, s.faces().size()) + + def testQuickStartXY(self): + 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) + self.assertEquals(1, s.solids().size()) + self.assertEquals(14, s.faces().size()) + self.saveModel(s) + + def testQuickStartYZ(self): + s = Workplane(Plane.YZ()).box(2, 4, 0.5).faces(">X").workplane().rect(1.5, 3.5, forConstruction=True)\ + .vertices().cskHole(0.125, 0.25, 82, depth=None) + self.assertEquals(1, s.solids().size()) + self.assertEquals(14, s.faces().size()) + self.saveModel(s) + + def testQuickStartXZ(self): + s = Workplane(Plane.XZ()).box(2, 4, 0.5).faces(">Y").workplane().rect(1.5, 3.5, forConstruction=True)\ + .vertices().cskHole(0.125, 0.25, 82, depth=None) + self.assertEquals(1, s.solids().size()) + self.assertEquals(14, s.faces().size()) + self.saveModel(s) + + def testDoubleTwistedLoft(self): + s = Workplane("XY").polygon(8, 20.0).workplane(offset=4.0).transformed(rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() + s2 = Workplane("XY").polygon(8, 20.0).workplane(offset=-4.0).transformed(rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() + #self.assertEquals(10,s.faces().size()) + #self.assertEquals(1,s.solids().size()) + s3 = s.combineSolids(s2) + self.saveModel(s3) + + def testTwistedLoft(self): + s = Workplane("XY").polygon(8,20.0).workplane(offset=4.0).transformed(rotate=Vector(0,0,15.0)).polygon(8,20).loft() + self.assertEquals(10,s.faces().size()) + self.assertEquals(1,s.solids().size()) + self.saveModel(s) + + def testUnions(self): + #duplicates a memory problem of some kind reported when combining lots of objects + s = Workplane("XY").rect(0.5,0.5).extrude(5.0) + o = [] + beginTime = time.time() + for i in range(15): + t = Workplane("XY").center(10.0*i,0).rect(0.5,0.5).extrude(5.0) + o.append(t) + + #union stuff + for oo in o: + s = s.union(oo) + print "Total time %0.3f" % (time.time() - beginTime) + + #Test unioning a Solid object + s = Workplane(Plane.XY()) + currentS = s.rect(2.0,2.0).extrude(0.5) + toUnion = s.rect(1.0,1.0).extrude(1.0) + + currentS.union(toUnion.val(), combine=False) + + #TODO: When unioning and combining is figured out, uncomment the following assert + #self.assertEqual(10,currentS.faces().size()) + + def testCombine(self): + s = Workplane(Plane.XY()) + objects1 = s.rect(2.0,2.0).extrude(0.5).faces('>Z').rect(1.0,1.0).extrude(0.5) + + objects1.combine() + + self.assertEqual(11, objects1.faces().size()) + + + def testCombineSolidsInLoop(self): + #duplicates a memory problem of some kind reported when combining lots of objects + s = Workplane("XY").rect(0.5,0.5).extrude(5.0) + o = [] + beginTime = time.time() + for i in range(15): + t = Workplane("XY").center(10.0*i,0).rect(0.5,0.5).extrude(5.0) + o.append(t) + + #append the 'good way' + for oo in o: + s.add(oo) + s = s.combineSolids() + + print "Total time %0.3f" % (time.time() - beginTime) + + self.saveModel(s) + + def testClean(self): + """ + Tests the `clean()` method which is called automatically. + """ + + # make a cube with a splitter edge on one of the faces + # autosimplify should remove the splitter + s = Workplane("XY").moveTo(0,0).line(5,0).line(5,0).line(0,10).\ + line(-10,0).close().extrude(10) + + self.assertEqual(6, s.faces().size()) + + # test removal of splitter caused by union operation + s = Workplane("XY").box(10,10,10).union(Workplane("XY").box(20,10,10)) + + self.assertEqual(6, s.faces().size()) + + # test removal of splitter caused by extrude+combine operation + s = Workplane("XY").box(10,10,10).faces(">Y").\ + workplane().rect(5,10,5).extrude(20) + + self.assertEqual(10, s.faces().size()) + + # test removal of splitter caused by double hole operation + s = Workplane("XY").box(10,10,10).faces(">Z").workplane().\ + hole(3,5).faces(">Z").workplane().hole(3,10) + + self.assertEqual(7, s.faces().size()) + + # test removal of splitter caused by cutThruAll + s = Workplane("XY").box(10,10,10).faces(">Y").workplane().\ + rect(10,5).cutBlind(-5).faces(">Z").workplane().\ + center(0,2.5).rect(5,5).cutThruAll() + + self.assertEqual(18, s.faces().size()) + + # test removal of splitter with box + s = Workplane("XY").box(5,5,5).box(10,5,2) + + self.assertEqual(14, s.faces().size()) + + def testNoClean(self): + """ + Test the case when clean is disabled. + """ + # test disabling autoSimplify + s = Workplane("XY").moveTo(0,0).line(5,0).line(5,0).line(0,10).\ + line(-10,0).close().extrude(10, clean=False) + self.assertEqual(7, s.faces().size()) + + s = Workplane("XY").box(10,10,10).\ + union(Workplane("XY").box(20,10,10), clean=False) + self.assertEqual(14, s.faces().size()) + + s = Workplane("XY").box(10,10,10).faces(">Y").\ + workplane().rect(5,10,5).extrude(20, clean=False) + + self.assertEqual(12, s.faces().size()) + + def testExplicitClean(self): + """ + Test running of `clean()` method explicitly. + """ + s = Workplane("XY").moveTo(0,0).line(5,0).line(5,0).line(0,10).\ + line(-10,0).close().extrude(10,clean=False).clean() + self.assertEqual(6, s.faces().size()) + + def testCup(self): + + """ + UOM = "mm" + + # + # PARAMETERS and PRESETS + # These parameters can be manipulated by end users + # + bottomDiameter = FloatParam(min=10.0,presets={'default':50.0,'tumbler':50.0,'shot':35.0,'tea':50.0,'saucer':100.0},group="Basics", desc="Bottom diameter") + topDiameter = FloatParam(min=10.0,presets={'default':85.0,'tumbler':85.0,'shot':50.0,'tea':51.0,'saucer':400.0 },group="Basics", desc="Top diameter") + thickness = FloatParam(min=0.1,presets={'default':2.0,'tumbler':2.0,'shot':2.66,'tea':2.0,'saucer':2.0},group="Basics", desc="Thickness") + height = FloatParam(min=1.0,presets={'default':80.0,'tumbler':80.0,'shot':59.0,'tea':125.0,'saucer':40.0},group="Basics", desc="Overall height") + lipradius = FloatParam(min=1.0,presets={'default':1.0,'tumbler':1.0,'shot':0.8,'tea':1.0,'saucer':1.0},group="Basics", desc="Lip Radius") + bottomThickness = FloatParam(min=1.0,presets={'default':5.0,'tumbler':5.0,'shot':10.0,'tea':10.0,'saucer':5.0},group="Basics", desc="BottomThickness") + + # + # Your build method. It must return a solid object + # + def build(): + br = bottomDiameter.value / 2.0 + tr = topDiameter.value / 2.0 + t = thickness.value + s1 = Workplane("XY").circle(br).workplane(offset=height.value).circle(tr).loft() + s2 = Workplane("XY").workplane(offset=bottomThickness.value).circle(br - t ).workplane(offset=height.value - t ).circle(tr - t).loft() + + cup = s1.cut(s2) + cup.faces(">Z").edges().fillet(lipradius.value) + return cup + """ + + #for some reason shell doesnt work on this simple shape. how disappointing! + td = 50.0 + bd = 20.0 + h = 10.0 + t = 1.0 + s1 = Workplane("XY").circle(bd).workplane(offset=h).circle(td).loft() + s2 = Workplane("XY").workplane(offset=t).circle(bd-(2.0*t)).workplane(offset=(h-t)).circle(td-(2.0*t)).loft() + s3 = s1.cut(s2) + self.saveModel(s3) + + + def testEnclosure(self): + """ + Builds an electronics enclosure + Original FreeCAD script: 81 source statements ,not including variables + This script: 34 + """ + + #parameter definitions + p_outerWidth = 100.0 #Outer width of box enclosure + p_outerLength = 150.0 #Outer length of box enclosure + p_outerHeight = 50.0 #Outer height of box enclosure + + p_thickness = 3.0 #Thickness of the box walls + p_sideRadius = 10.0 #Radius for the curves around the sides of the bo + p_topAndBottomRadius = 2.0 #Radius for the curves on the top and bottom edges of the box + + p_screwpostInset = 12.0 #How far in from the edges the screwposts should be place. + p_screwpostID = 4.0 #nner Diameter of the screwpost holes, should be roughly screw diameter not including threads + p_screwpostOD = 10.0 #Outer Diameter of the screwposts.\nDetermines overall thickness of the posts + + p_boreDiameter = 8.0 #Diameter of the counterbore hole, if any + p_boreDepth = 1.0 #Depth of the counterbore hole, if + p_countersinkDiameter = 0.0 #Outer diameter of countersink. Should roughly match the outer diameter of the screw head + p_countersinkAngle = 90.0 #Countersink angle (complete angle between opposite sides, not from center to one side) + p_flipLid = True #Whether to place the lid with the top facing down or not. + p_lipHeight = 1.0 #Height of lip on the underside of the lid.\nSits inside the box body for a snug fit. + + #outer shell + oshell = Workplane("XY").rect(p_outerWidth,p_outerLength).extrude(p_outerHeight + p_lipHeight) + + #weird geometry happens if we make the fillets in the wrong order + if p_sideRadius > p_topAndBottomRadius: + oshell.edges("|Z").fillet(p_sideRadius) + oshell.edges("#Z").fillet(p_topAndBottomRadius) + else: + oshell.edges("#Z").fillet(p_topAndBottomRadius) + oshell.edges("|Z").fillet(p_sideRadius) + + #inner shell + ishell = oshell.faces("Z").workplane(-p_thickness)\ + .rect(POSTWIDTH,POSTLENGTH,forConstruction=True)\ + .vertices() + + for v in postCenters.all(): + v.circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0)\ + .extrude((-1.0)*(p_outerHeight + p_lipHeight -p_thickness ),True) + + #split lid into top and bottom parts + (lid,bottom) = box.faces(">Z").workplane(-p_thickness -p_lipHeight ).split(keepTop=True,keepBottom=True).all() #splits into two solids + + #translate the lid, and subtract the bottom from it to produce the lid inset + lowerLid = lid.translate((0,0,-p_lipHeight)) + cutlip = lowerLid.cut(bottom).translate((p_outerWidth + p_thickness ,0,p_thickness - p_outerHeight + p_lipHeight)) + + #compute centers for counterbore/countersink or counterbore + topOfLidCenters = cutlip.faces(">Z").workplane().rect(POSTWIDTH,POSTLENGTH,forConstruction=True).vertices() + + #add holes of the desired type + if p_boreDiameter > 0 and p_boreDepth > 0: + topOfLid = topOfLidCenters.cboreHole(p_screwpostID,p_boreDiameter,p_boreDepth,(2.0)*p_thickness) + elif p_countersinkDiameter > 0 and p_countersinkAngle > 0: + topOfLid = topOfLidCenters.cskHole(p_screwpostID,p_countersinkDiameter,p_countersinkAngle,(2.0)*p_thickness) + else: + topOfLid= topOfLidCenters.hole(p_screwpostID,(2.0)*p_thickness) + + #flip lid upside down if desired + if p_flipLid: + topOfLid.rotateAboutCenter((1,0,0),180) + + #return the combined result + result =topOfLid.union(bottom) + + self.saveModel(result) diff --git a/build/lib.linux-i686-2.7/tests/TestExporters.py b/build/lib.linux-i686-2.7/tests/TestExporters.py new file mode 100644 index 0000000..2c7b9f4 --- /dev/null +++ b/build/lib.linux-i686-2.7/tests/TestExporters.py @@ -0,0 +1,43 @@ +""" + Tests basic workplane functionality +""" +#core modules +import StringIO + +#my modules +from cadquery import * +from cadquery import exporters +from tests import BaseTest + +class TestExporters(BaseTest): + + def _exportBox(self,eType,stringsToFind): + """ + Exports a test object, and then looks for + all of the supplied strings to be in the result + returns the result in case the case wants to do more checks also + """ + p = Workplane("XY").box(1,2,3) + s = StringIO.StringIO() + exporters.exportShape(p,eType,s,0.1) + + result = s.getvalue() + #print result + for q in stringsToFind: + self.assertTrue(result.find(q) > -1 ) + return result + + def testSTL(self): + self._exportBox(exporters.ExportTypes.STL,['facet normal']) + + def testSVG(self): + self._exportBox(exporters.ExportTypes.SVG,['']) + + def testSTEP(self): + self._exportBox(exporters.ExportTypes.STEP,['FILE_SCHEMA']) + + def testTJS(self): + self._exportBox(exporters.ExportTypes.TJS,['vertices','formatVersion','faces']) diff --git a/build/lib.linux-i686-2.7/tests/TestImporters.py b/build/lib.linux-i686-2.7/tests/TestImporters.py new file mode 100644 index 0000000..edee5d5 --- /dev/null +++ b/build/lib.linux-i686-2.7/tests/TestImporters.py @@ -0,0 +1,54 @@ +""" + Tests file importers such as STEP +""" +#core modules +import StringIO + +from cadquery import * +from cadquery import exporters +from cadquery import importers +from tests import BaseTest + +#where unit test output will be saved +import sys +if sys.platform.startswith("win"): + OUTDIR = "c:/temp" +else: + OUTDIR = "/tmp" + + +class TestImporters(BaseTest): + def importBox(self, importType, fileName): + """ + Exports a simple box to a STEP file and then imports it again + :param importType: The type of file we're importing (STEP, STL, etc) + :param fileName: The path and name of the file to write to + """ + #We're importing a STEP file + if importType == importers.ImportTypes.STEP: + #We first need to build a simple shape to export + shape = Workplane("XY").box(1, 2, 3).val() + + #Export the shape to a temporary file + shape.exportStep(fileName) + + # Reimport the shape from the new STEP file + importedShape = importers.importShape(importType,fileName) + + #Check to make sure we got a solid back + self.assertTrue(importedShape.val().ShapeType() == "Solid") + + #Check the number of faces and vertices per face to make sure we have a box shape + self.assertTrue(importedShape.faces("+X").size() == 1 and importedShape.faces("+X").vertices().size() == 4) + self.assertTrue(importedShape.faces("+Y").size() == 1 and importedShape.faces("+Y").vertices().size() == 4) + self.assertTrue(importedShape.faces("+Z").size() == 1 and importedShape.faces("+Z").vertices().size() == 4) + + def testSTEP(self): + """ + Tests STEP file import + """ + self.importBox(importers.ImportTypes.STEP, OUTDIR + "/tempSTEP.step") + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/build/lib.linux-i686-2.7/tests/TestWorkplanes.py b/build/lib.linux-i686-2.7/tests/TestWorkplanes.py new file mode 100644 index 0000000..838b953 --- /dev/null +++ b/build/lib.linux-i686-2.7/tests/TestWorkplanes.py @@ -0,0 +1,125 @@ +""" + Tests basic workplane functionality +""" +#core modules + +#my modules +from cadquery import * +from tests import BaseTest,toTuple + +xAxis_ = Vector(1, 0, 0) +yAxis_ = Vector(0, 1, 0) +zAxis_ = Vector(0, 0, 1) +xInvAxis_ = Vector(-1, 0, 0) +yInvAxis_ = Vector(0, -1, 0) +zInvAxis_ = Vector(0, 0, -1) + +class TestWorkplanes(BaseTest): + + def testYZPlaneOrigins(self): + #xy plane-- with origin at x=0.25 + base = Vector(0.25,0,0) + p = Plane(base, Vector(0,1,0), Vector(1,0,0)) + + #origin is always (0,0,0) in local coordinates + self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 ) + + #(0,0,0) is always the original base in global coordinates + self.assertTupleAlmostEquals(base.toTuple(), p.toWorldCoords((0,0)).toTuple() ,2 ) + + def testXYPlaneOrigins(self): + base = Vector(0,0,0.25) + p = Plane(base, Vector(1,0,0), Vector(0,0,1)) + + #origin is always (0,0,0) in local coordinates + self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 ) + + #(0,0,0) is always the original base in global coordinates + self.assertTupleAlmostEquals(toTuple(base), p.toWorldCoords((0,0)).toTuple() ,2 ) + + def testXZPlaneOrigins(self): + base = Vector(0,0.25,0) + p = Plane(base, Vector(0,0,1), Vector(0,1,0)) + + #(0,0,0) is always the original base in global coordinates + self.assertTupleAlmostEquals(toTuple(base), p.toWorldCoords((0,0)).toTuple() ,2 ) + + #origin is always (0,0,0) in local coordinates + self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 ) + + def testPlaneBasics(self): + p = Plane.XY() + #local to world + self.assertTupleAlmostEquals((1.0,1.0,0),p.toWorldCoords((1,1)).toTuple(),2 ) + self.assertTupleAlmostEquals((-1.0,-1.0,0), p.toWorldCoords((-1,-1)).toTuple(),2 ) + + #world to local + self.assertTupleAlmostEquals((-1.0,-1.0), p.toLocalCoords(Vector(-1,-1,0)).toTuple() ,2 ) + self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(1,1,0)).toTuple() ,2 ) + + p = Plane.YZ() + self.assertTupleAlmostEquals((0,1.0,1.0),p.toWorldCoords((1,1)).toTuple() ,2 ) + + #world to local + self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(0,1,1)).toTuple() ,2 ) + + p = Plane.XZ() + r = p.toWorldCoords((1,1)).toTuple() + self.assertTupleAlmostEquals((1.0,0.0,1.0),r ,2 ) + + #world to local + self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(1,0,1)).toTuple() ,2 ) + + def testOffsetPlanes(self): + "Tests that a plane offset from the origin works ok too" + p = Plane.XY(origin=(10.0,10.0,0)) + + + self.assertTupleAlmostEquals((11.0,11.0,0.0),p.toWorldCoords((1.0,1.0)).toTuple(),2 ) + self.assertTupleAlmostEquals((2.0,2.0), p.toLocalCoords(Vector(12.0,12.0,0)).toTuple() ,2 ) + + #TODO test these offsets in the other dimensions too + p = Plane.YZ(origin=(0,2,2)) + self.assertTupleAlmostEquals((0.0,5.0,5.0), p.toWorldCoords((3.0,3.0)).toTuple() ,2 ) + self.assertTupleAlmostEquals((10,10.0,0.0), p.toLocalCoords(Vector(0.0,12.0,12.0)).toTuple() ,2 ) + + p = Plane.XZ(origin=(2,0,2)) + r = p.toWorldCoords((1.0,1.0)).toTuple() + self.assertTupleAlmostEquals((3.0,0.0,3.0),r ,2 ) + self.assertTupleAlmostEquals((10.0,10.0), p.toLocalCoords(Vector(12.0,0.0,12.0)).toTuple() ,2 ) + + def testXYPlaneBasics(self): + p = Plane.named('XY') + self.assertTupleAlmostEquals(p.zDir.toTuple(), zAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4) + + def testYZPlaneBasics(self): + p = Plane.named('YZ') + self.assertTupleAlmostEquals(p.zDir.toTuple(), xAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) + + def testZXPlaneBasics(self): + p = Plane.named('ZX') + self.assertTupleAlmostEquals(p.zDir.toTuple(), yAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) + + def testXZPlaneBasics(self): + p = Plane.named('XZ') + self.assertTupleAlmostEquals(p.zDir.toTuple(), yInvAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) + + def testYXPlaneBasics(self): + p = Plane.named('YX') + self.assertTupleAlmostEquals(p.zDir.toTuple(), zInvAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) + + def testZYPlaneBasics(self): + p = Plane.named('ZY') + self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) + self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4) diff --git a/build/lib.linux-i686-2.7/tests/__init__.py b/build/lib.linux-i686-2.7/tests/__init__.py new file mode 100644 index 0000000..5dfc40e --- /dev/null +++ b/build/lib.linux-i686-2.7/tests/__init__.py @@ -0,0 +1,54 @@ +from cadquery import * +import unittest +import sys +import os + +import FreeCAD + +import Part as P +from FreeCAD import Vector as V + + +def readFileAsString(fileName): + f= open(fileName, 'r') + s = f.read() + f.close() + return s + + +def writeStringToFile(strToWrite, fileName): + f = open(fileName, 'w') + f.write(strToWrite) + f.close() + + +def makeUnitSquareWire(): + return Solid.cast(P.makePolygon([V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)])) + + +def makeUnitCube(): + return makeCube(1.0) + + +def makeCube(size): + return Solid.makeBox(size, size, size) + + +def toTuple(v): + """convert a vector or a vertex to a 3-tuple: x,y,z""" + pnt = v + if type(v) == FreeCAD.Base.Vector: + return (v.Point.x, v.Point.y, v.Point.z) + elif type(v) == Vector: + return v.toTuple() + else: + raise RuntimeError("dont know how to convert type %s to tuple" % str(type(v)) ) + + +class BaseTest(unittest.TestCase): + + def assertTupleAlmostEquals(self, expected, actual, places): + for i, j in zip(actual, expected): + self.assertAlmostEquals(i, j, places) + +__all__ = ['TestCadObjects', 'TestCadQuery', 'TestCQSelectors', 'TestWorkplanes', 'TestExporters', 'TestCQSelectors', 'TestImporters','TestCQGI'] diff --git a/cadquery.egg-info/PKG-INFO b/cadquery.egg-info/PKG-INFO index a6a1f17..8d759fc 100644 --- a/cadquery.egg-info/PKG-INFO +++ b/cadquery.egg-info/PKG-INFO @@ -1,14 +1,19 @@ Metadata-Version: 1.1 Name: cadquery -Version: 0.3.0 +Version: 0.4.0 Summary: CadQuery is a parametric scripting language for creating and traversing CAD models Home-page: https://github.com/dcowden/cadquery Author: David Cowden Author-email: dave.cowden@gmail.com -License: Apache Public License +License: Apache Public License 2.0 Description: What is a CadQuery? ======================================== + [![Travis Build Status](https://travis-ci.org/dcowden/cadquery.svg)](https://travis-ci.org/dcowden/cadquery) + [![Coverage Status](https://coveralls.io/repos/dcowden/cadquery/badge.svg)](https://coveralls.io/r/dcowden/cadquery) + [![GitHub version](https://badge.fury.io/gh/dcowden%2Fcadquery.svg)](https://github.com/dcowden/cadquery/releases/tag/v0.3.0) + [![License](https://img.shields.io/badge/license-LGPL-lightgrey.svg)](https://github.com/dcowden/cadquery/blob/master/LICENSE) + CadQuery is an intuitive, easy-to-use python based language for building parametric 3D CAD models. CadQuery is for 3D CAD what jQuery is for javascript. Imagine selecting Faces of a 3d object the same way you select DOM objects with JQuery! CadQuery has several goals: @@ -20,18 +25,24 @@ Description: What is a CadQuery? Using CadQuery, you can write short, simple scripts that produce high quality CAD models. It is easy to make many different objects using a single script that can be customized. + Full Documentation + ============================ + You can find the full cadquery documentation at http://dcowden.github.io/cadquery + + Getting Started With CadQuery ======================================== - The easiest way to get started with CadQuery is to Install FreeCAD ( version 14 recommended ) (http://www.freecadweb.org/) , and then to use our CadQuery-FreeCAD plugin here: - - https://github.com/jmwright/cadquery-freecad-module + The easiest way to get started with CadQuery is to Install FreeCAD (version 14+) (http://www.freecadweb.org/), and then to use our great CadQuery-FreeCAD plugin here: https://github.com/jmwright/cadquery-freecad-module It includes the latest version of cadquery alreadby bundled, and has super-easy installation on Mac, Windows, and Unix. It has tons of awesome features like integration with FreeCAD so you can see your objects, code-autocompletion, an examples bundle, and script saving/loading. Its definitely the best way to kick the tires! + We also have a Google Group to make it easy to get help from other CadQuery users. Please join the group and introduce yourself, and we would also love to hear what you are doing with CadQuery. https://groups.google.com/forum/#!forum/cadquery + + Why CadQuery instead of OpenSCAD? ======================================== @@ -55,12 +66,13 @@ Description: What is a CadQuery? 4. **Less Code and easier scripting** CadQuery scripts require less code to create most objects, because it is possible to locate features based on the position of other features, workplanes, vertices, etc. - 5. **Better Performance** CadQuery scripts can build STL, STEP, and AMF faster than OpenSCAD. + 5. **Better Performance** CadQuery scripts can build STL, STEP, and AMF faster than OpenSCAD. License ======== - CadQuery is licensed under the terms of the Apache Public License, v 2.0 http://www.apache.org/licenses/LICENSE-2.0 + CadQuery is licensed under the terms of the Apache Public License, version 2.0. + A copy of the license can be found at http://www.apache.org/licenses/LICENSE-2.0 Where is the GUI? ================== @@ -74,22 +86,22 @@ Description: What is a CadQuery? Use these steps if you would like to write CadQuery scripts as a python API. In this case, FreeCAD is used only as a CAD kernel. - 1. install FreeCAD, version 0.14 or greater for your platform. http://sourceforge.net/projects/free-cad/. + 1. install FreeCAD, version 0.12 or greater for your platform. http://sourceforge.net/projects/free-cad/. - 2. adjust your path if necessary. FreeCAD bundles a python interpreter, but you'll probably want to use your own, + 2. adjust your path if necessary. FreeCAD bundles a python interpreter, but you'll probably want to use your own, preferably one that has virtualenv available. To use FreeCAD from any python interpreter, just append the FreeCAD lib directory to your path. On (*Nix):: - + import sys sys.path.append('/usr/lib/freecad/lib') - + or on Windows:: - + import sys sys.path.append('/c/apps/FreeCAD/bin') - + *NOTE* FreeCAD on Windows will not work with python 2.7-- you must use pthon 2.6.X!!!! - + 3. install cadquery:: pip install cadquery @@ -99,9 +111,9 @@ Description: What is a CadQuery? from cadquery import * box = Workplane("XY").box(1,2,3) exporters.toString(box,'STL') - + You're up and running! - + Installing -- Using CadQuery from Inside FreeCAD ================================================= @@ -120,7 +132,7 @@ Description: What is a CadQuery? * A fluent api to create clean, easy to read code * Language features that make selection and iteration incredibly easy - * + * * Ability to use the library along side other python libraries * Clear and complete documentation, with plenty of samples. @@ -132,7 +144,7 @@ Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: Science/Research Classifier: Intended Audience :: System Administrators -Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) +Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS Classifier: Operating System :: Unix diff --git a/cadquery.egg-info/SOURCES.txt b/cadquery.egg-info/SOURCES.txt index 06a2837..0507b5e 100644 --- a/cadquery.egg-info/SOURCES.txt +++ b/cadquery.egg-info/SOURCES.txt @@ -2,9 +2,10 @@ MANIFEST.in README.txt setup.cfg setup.py -cadquery/CQ.py cadquery/__init__.py +cadquery/cq.py cadquery/cq_directive.py +cadquery/cqgi.py cadquery/selectors.py cadquery.egg-info/PKG-INFO cadquery.egg-info/SOURCES.txt @@ -18,6 +19,7 @@ cadquery/freecad_impl/geom.py cadquery/freecad_impl/importers.py cadquery/freecad_impl/shapes.py cadquery/plugins/__init__.py +tests/TestCQGI.py tests/TestCQSelectors.py tests/TestCadObjects.py tests/TestCadQuery.py diff --git a/setup.py b/setup.py index 62b0de7..e68a6e6 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( author_email='dave.cowden@gmail.com', description='CadQuery is a parametric scripting language for creating and traversing CAD models', long_description=open('README.md').read(), - packages=['cadquery','cadquery.contrib','cadquery.freecad_impl','cadquery.plugins','cadquery.cqgi','tests'], + packages=['cadquery','cadquery.contrib','cadquery.freecad_impl','cadquery.plugins','tests'], include_package_data=True, zip_safe=False, platforms='any',