diff --git a/.gitignore b/.gitignore index 5c54edb..5600ded 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.pyc doc/_build/* dist/* +.idea/* diff --git a/cadquery.egg-info/PKG-INFO b/cadquery.egg-info/PKG-INFO index e0fafbe..22ab98a 100644 --- a/cadquery.egg-info/PKG-INFO +++ b/cadquery.egg-info/PKG-INFO @@ -1,6 +1,6 @@ -Metadata-Version: 1.0 +Metadata-Version: 1.1 Name: cadquery -Version: 0.1.5 +Version: 0.1.6 Summary: CadQuery is a parametric scripting language for creating and traversing CAD models Home-page: https://github.com/dcowden/cadquery Author: David Cowden diff --git a/cadquery.egg-info/SOURCES.txt b/cadquery.egg-info/SOURCES.txt index 2328c8b..2e88412 100644 --- a/cadquery.egg-info/SOURCES.txt +++ b/cadquery.egg-info/SOURCES.txt @@ -16,6 +16,7 @@ cadquery/contrib/__init__.py cadquery/freecad_impl/__init__.py cadquery/freecad_impl/exporters.py cadquery/freecad_impl/geom.py +cadquery/freecad_impl/importers.py cadquery/freecad_impl/shapes.py cadquery/freecad_impl/verutil.py cadquery/plugins/__init__.py @@ -23,6 +24,7 @@ tests/TestCQSelectors.py tests/TestCadObjects.py tests/TestCadQuery.py tests/TestExporters.py +tests/TestImporters.py tests/TestImports.py tests/TestWorkplanes.py tests/__init__.py \ No newline at end of file diff --git a/cadquery/CQ.py b/cadquery/CQ.py index e4b870f..3723f67 100644 --- a/cadquery/CQ.py +++ b/cadquery/CQ.py @@ -1861,13 +1861,62 @@ class Workplane(CQ): FutureEnhancement: Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane extrude to surface. this is quite tricky since the surface selected may not be planar - """ + """ r = self._extrude(distance) #returns a Solid ( or a compound if there were multiple ) if combine: return self._combineWithBase(r) else: return self.newObject([r]) + def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True): + """ + Use all un-revolved wires in the parent chain to create a solid. + + :param angleDegrees: the angle to revolve through. + :type angleDegrees: float, anything less than 360 degrees will leave the shape open + :param axisStart: the start point of the axis of rotation + :type axisStart: tuple, a two tuple + :param axisEnd: the end point of the axis of rotation + :type axisEnd: tuple, a two tuple + :param combine: True to combine the resulting solid with parent solids if found. + :type combine: boolean, combine with parent solid + :return: a CQ object with the resulting solid selected. + + The returned object is always a CQ object, and depends on wither combine is True, and + whether a context solid is already defined: + + * if combine is False, the new value is pushed onto the stack. + * if combine is true, the value is combined with the context solid if it exists, + and the resulting solid becomes the new context solid. + """ + #Make sure we account for users specifying angles larger than 360 degrees + angleDegrees = angleDegrees % 360.0 + + #Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve + angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees + + #The default start point of the vector defining the axis of rotation will be the origin of the workplane + if axisStart is None: + axisStart = self.plane.toWorldCoords((0,0)).toTuple() + else: + axisStart = self.plane.toWorldCoords(axisStart).toTuple() + + #The default end point of the vector defining the axis of rotation should be along the normal from the plane + if axisEnd is None: + #Make sure we match the user's assumed axis of rotation if they specified an start but not an end + if axisStart[1] != 0: + axisEnd = self.plane.toWorldCoords((0,axisStart[1])).toTuple() + else: + axisEnd = self.plane.toWorldCoords((0,1)).toTuple() + else: + axisEnd = self.plane.toWorldCoords(axisEnd).toTuple() + + r = self._revolve(angleDegrees, axisStart, axisEnd) # returns a Solid ( or a compound if there were multiple ) + if combine: + return self._combineWithBase(r) + else: + return self.newObject([r]) + def _combineWithBase2(self,obj): """ Combines the provided object with the base solid, if one can be found. @@ -2105,6 +2154,33 @@ class Workplane(CQ): 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): """ diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index 1a78fe7..b0a268e 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -1,20 +1,20 @@ """ - Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC + Copyright (C) 2011-2014 Parametric Products Intellectual Holdings, LLC - This file is part of CadQuery. + 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 free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. - CadQuery is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. + CadQuery is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public - License along with this library; If not, see + You should have received a copy of the GNU Lesser General Public + License along with this library; If not, see Wrapper Classes for FreeCAD These classes provide a stable interface for 3d objects, @@ -47,45 +47,47 @@ 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 +from cadquery import Vector, BoundBox import FreeCAD from .verutil import fc_import + FreeCADPart = fc_import("FreeCAD.Part") + class Shape(object): """ Represents a shape in the system. Wrappers the FreeCAD api """ - def __init__(self,obj): + def __init__(self, obj): self.wrapped = obj self.forConstruction = False @classmethod - def cast(cls,obj,forConstruction = False): + 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. + # 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) + tr = Vertex(obj) elif s == 'Edge': - tr= Edge(obj) + tr = Edge(obj) elif s == 'Wire': tr = Wire(obj) elif s == 'Face': - tr= Face(obj) + tr = Face(obj) elif s == 'Shell': - tr= Shell(obj) + tr = Shell(obj) elif s == 'Solid': - tr= Solid(obj) + tr = Solid(obj) elif s == 'Compound': #compound of solids, lets return a solid instead if len(obj.Solids) > 1: @@ -95,22 +97,23 @@ class Shape(object): elif len(obj.Wires) > 0: tr = Wire(obj) else: - tr= Compound(obj) + 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): + + # 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): + def exportStep(self, fileName): self.wrapped.exportStep(fileName) - def exportShape(self,fileName, fileFormat): + def exportShape(self, fileName, fileFormat): if fileFormat == ExportFormats.STL: self.wrapped.exportStl(fileName) elif fileFormat == ExportFormats.BREP: @@ -118,7 +121,7 @@ class Shape(object): elif fileFormat == ExportFormats.STEP: self.wrapped.exportStep(fileName) elif fileFormat == ExportFormats.AMF: - #not built into FreeCAD + # not built into FreeCAD #TODO: user selected tolerance tess = self.wrapped.tessellate(0.1) aw = amfUtils.AMFWriter(tess) @@ -154,14 +157,14 @@ class Shape(object): """ return self.wrapped.ShapeType - def isType(self,obj,strType): + 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'): + if hasattr(obj, 'ShapeType'): return obj.ShapeType == strType else: return False @@ -172,10 +175,10 @@ class Shape(object): def isNull(self): return self.wrapped.isNull() - def isSame(self,other): + def isSame(self, other): return self.wrapped.isSame(other.wrapped) - def isEqual(self,other): + def isEqual(self, other): return self.wrapped.isEqual(other.wrapped) def isValid(self): @@ -189,6 +192,7 @@ class Shape(object): return Vector(self.wrapped.CenterOfMass) except: pass + def Closed(self): return self.wrapped.Closed @@ -222,7 +226,7 @@ class Shape(object): def Length(self): return self.wrapped.Length - def rotate(self,startVector,endVector,angleDegrees): + 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 @@ -237,10 +241,10 @@ class Shape(object): endVector = Vector(endVector) tmp = self.wrapped.copy() - tmp.rotate(startVector.wrapped,endVector.wrapped,angleDegrees) + tmp.rotate(startVector.wrapped, endVector.wrapped, angleDegrees) return Shape.cast(tmp) - def translate(self,vector): + def translate(self, vector): if type(vector) == tuple: vector = Vector(vector) @@ -248,7 +252,7 @@ class Shape(object): tmp.translate(vector.wrapped) return Shape.cast(tmp) - def scale(self,factor): + def scale(self, factor): tmp = self.wrapped.copy() tmp.scale(factor) return Shape.cast(tmp) @@ -256,9 +260,9 @@ class Shape(object): def copy(self): return Shape.cast(self.wrapped.copy()) - def transformShape(self,tMatrix): + def transformShape(self, tMatrix): """ - tMatrix is a matrix object. + tMatrix is a matrix object. returns a copy of the ojbect, transformed by the provided matrix, with all objects keeping their type """ @@ -268,9 +272,9 @@ class Shape(object): r.forConstruction = self.forConstruction return r - def transformGeometry(self,tMatrix): + def transformGeometry(self, tMatrix): """ - tMatrix is a matrix object. + tMatrix is a matrix object. returns a copy of the object, but with geometry transformed insetad of just rotated. @@ -288,8 +292,9 @@ class Shape(object): def __hash__(self): return self.wrapped.hashCode() + class Vertex(Shape): - def __init__(self,obj,forConstruction=False): + def __init__(self, obj, forConstruction=False): """ Create a vertex from a FreeCAD Vertex """ @@ -300,7 +305,7 @@ class Vertex(Shape): self.Z = obj.Z def toTuple(self): - return (self.X,self.Y,self.Z) + return (self.X, self.Y, self.Z) def Center(self): """ @@ -308,19 +313,20 @@ class Vertex(Shape): """ return Vector(self.wrapped.Point) + class Edge(Shape): - def __init__(self,obj): + def __init__(self, obj): """ An Edge """ self.wrapped = obj - #self.startPoint = None - #self.endPoint = None + # self.startPoint = None + # self.endPoint = None - self.edgetypes= { - FreeCADPart.Line : 'LINE', - FreeCADPart.ArcOfCircle : 'ARC', - FreeCADPart.Circle : 'CIRCLE' + self.edgetypes = { + FreeCADPart.Line: 'LINE', + FreeCADPart.ArcOfCircle: 'ARC', + FreeCADPart.Circle: 'CIRCLE' } def geomType(self): @@ -337,9 +343,9 @@ class Edge(Shape): Note, circles may have the start and end points the same """ - #work around freecad bug where valueAt is unreliable + # work around freecad bug where valueAt is unreliable curve = self.wrapped.Curve - return Vector( curve.value(self.wrapped.ParameterRange[0])) + return Vector(curve.value(self.wrapped.ParameterRange[0])) def endPoint(self): """ @@ -349,14 +355,14 @@ class Edge(Shape): 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 + # 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])) + v = Vector(curve.value(self.wrapped.ParameterRange[1])) return v - def tangentAt(self,locationVector=None): + def tangentAt(self, locationVector=None): """ Compute tangent vector at the specified location. :param locationVector: location to use. Use the center point if None @@ -369,11 +375,11 @@ class Edge(Shape): 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)) + 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): + def makeSpline(cls, listOfVector): """ Interpolate a spline through the provided points. :param cls: @@ -383,11 +389,11 @@ class Edge(Shape): vecs = [v.wrapped for v in listOfVector] spline = FreeCADPart.BSplineCurve() - spline.interpolate(vecs,False) + spline.interpolate(vecs, False) return Edge(spline.toShape()) @classmethod - def makeThreePointArc(cls,v1,v2,v3): + def makeThreePointArc(cls, v1, v2, v3): """ Makes a three point arc through the provided points :param cls: @@ -396,30 +402,30 @@ class Edge(Shape): :param v3: end vector :return: an edge object through the three points """ - arc = FreeCADPart.Arc(v1.wrapped,v2.wrapped,v3.wrapped) + arc = FreeCADPart.Arc(v1.wrapped, v2.wrapped, v3.wrapped) e = Edge(arc.toShape()) - return e #arcane and undocumented, this creates an Edge object + return e # arcane and undocumented, this creates an Edge object @classmethod - def makeLine(cls,v1,v2): + 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() )) + return Edge(FreeCADPart.makeLine(v1.toTuple(), v2.toTuple())) class Wire(Shape): - def __init__(self,obj): + def __init__(self, obj): """ A Wire """ self.wrapped = obj @classmethod - def combine(cls,listOfWires): + def combine(cls, listOfWires): """ Attempt to combine a list of wires into a new wire. the wires are returned in a list. @@ -430,7 +436,7 @@ class Wire(Shape): return Shape.cast(FreeCADPart.Wire([w.wrapped for w in listOfWires])) @classmethod - def assembleEdges(cls,listOfEdges): + def assembleEdges(cls, listOfEdges): """ Attempts to build a wire that consists of the edges in the provided list :param cls: @@ -439,11 +445,11 @@ class Wire(Shape): """ fCEdges = [a.wrapped for a in listOfEdges] - wa = Wire( FreeCADPart.Wire(fCEdges) ) + wa = Wire(FreeCADPart.Wire(fCEdges)) return wa @classmethod - def makeCircle(cls,radius,center,normal): + 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 @@ -451,38 +457,38 @@ class Wire(Shape): :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)])) + 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. + 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): + 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)) + return Wire(FreeCADPart.makeHelix(pitch, height, radius, angle)) class Face(Shape): - def __init__(self,obj): + def __init__(self, obj): """ A Face """ self.wrapped = obj self.facetypes = { - #TODO: bezier,bspline etc - FreeCADPart.Plane : 'PLANE', - FreeCADPart.Sphere : 'SPHERE', - FreeCADPart.Cone : 'CONE' + # TODO: bezier,bspline etc + FreeCADPart.Plane: 'PLANE', + FreeCADPart.Sphere: 'SPHERE', + FreeCADPart.Cone: 'CONE' } def geomType(self): @@ -492,7 +498,7 @@ class Face(Shape): else: return "Unknown Face Surface Type: %s" % str(t) - def normalAt(self,locationVector=None): + def normalAt(self, locationVector=None): """ Computes the normal vector at the desired location on the face. @@ -502,31 +508,31 @@ class Face(Shape): """ if locationVector == None: locationVector = self.Center() - (u,v) = self.wrapped.Surface.parameter(locationVector.wrapped) + (u, v) = self.wrapped.Surface.parameter(locationVector.wrapped) - return Vector(self.wrapped.normalAt(u,v).normalize() ) + 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))) + 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): + 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)) + return Shape.cast(FreeCADPart.makeRuledSurface(edgeOrWire1.obj, edgeOrWire2.obj, dist)) - def cut(self,faceToCut): + def cut(self, faceToCut): "Remove a face from another one" return Shape.cast(self.obj.cut(faceToCut.obj)) - def fuse(self,faceToJoin): + def fuse(self, faceToJoin): return Shape.cast(self.obj.fuse(faceToJoin.obj)) - def intersect(self,faceToIntersect): + 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 @@ -535,73 +541,73 @@ class Face(Shape): class Shell(Shape): - def __init__(self,wrapped): + def __init__(self, wrapped): """ A Shell """ self.wrapped = wrapped @classmethod - def makeShell(cls,listOfFaces): + def makeShell(cls, listOfFaces): return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces])) class Solid(Shape): - def __init__(self,obj): + def __init__(self, obj): """ A Solid """ self.wrapped = obj @classmethod - def isSolid(cls,obj): + 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): + 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)): + def makeBox(cls, length, width, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)): """ makeBox(length,width,height,[pnt,dir]) -- Make a box located\nin pnt with the d imensions (length,width,height)\nBy default pnt=Vector(0,0,0) and dir=Vector(0,0,1)' """ - return Shape.cast(FreeCADPart.makeBox(length,width,height,pnt.wrapped,dir.wrapped)) + 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): + def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360): """ 'makeCone(radius1,radius2,height,[pnt,dir,angle]) -- Make a cone with given radii and height\nBy default pnt=Vector(0,0,0), dir=Vector(0,0,1) and angle=360' """ - return Shape.cast(FreeCADPart.makeCone(radius1,radius2,height,pnt.wrapped,dir.wrapped,angleDegrees)) + 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): + 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)) + 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): + 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)) + return Shape.cast(FreeCADPart.makeTorus(radius1, radius2, pnt, dir, angleDegrees1, angleDegrees2)) @classmethod - def sweep(cls,profileWire,pathWire): + def sweep(cls, profileWire, pathWire): """ make a solid by sweeping the profileWire along the specified path :param cls: @@ -609,41 +615,42 @@ class Solid(Shape): :param pathWire: :return: """ - #needs to use freecad wire.makePipe or makePipeShell - #needs to allow free-space wires ( those not made from a workplane ) + # 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): + def makeLoft(cls, listOfWire): """ makes a loft from a list of wires The wires will be converted into faces when possible-- it is presumed that nobody ever actually wants to make an infinitely thin shell for a real FreeCADPart. """ - #the True flag requests building a solid instead of a shell. + # the True flag requests building a solid instead of a shell. - return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire],True)) + return Shape.cast(FreeCADPart.makeLoft([i.wrapped for i in listOfWire], True)) @classmethod - def makeWedge(cls,xmin,ymin,zmin,z2min,x2min,xmax,ymax,zmax,z2max,x2max,pnt=None,dir=None): + def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=None, dir=None): """ 'makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max,[pnt, dir]) Make a wedge located in pnt\nBy default pnt=Vector(0,0,0) and dir=Vec tor(0,0,1)' """ - return Shape.cast(FreeCADPart.makeWedge(xmin,ymin,zmin,z2min,x2min,xmax,ymax,zmax,z2max,x2max,pnt,dir)) + return Shape.cast( + FreeCADPart.makeWedge(xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt, dir)) @classmethod - def makeSphere(cls,radius,pnt=None,angleDegrees1=None,angleDegrees2=None,angleDegrees3=None): + def makeSphere(cls, radius, pnt=None, angleDegrees1=None, angleDegrees2=None, angleDegrees3=None): """ 'makeSphere(radius,[pnt, dir, angle1,angle2,angle3]) -- Make a sphere with a giv en radius\nBy default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360' """ - return Solid(FreeCADPart.makeSphere(radius,pnt,angleDegrees1,angleDegrees2,angleDegrees3)) + return Solid(FreeCADPart.makeSphere(radius, pnt, angleDegrees1, angleDegrees2, angleDegrees3)) @classmethod - def extrudeLinearWithRotation(cls,outerWire,innerWires,vecCenter, vecNormal,angleDegrees): + def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees): """ Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector. @@ -665,23 +672,23 @@ class Solid(Shape): :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] + # 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 + # make translated and rotated copy of each wire for w in startWires: w2 = w.copy() w2.translate(vecNormal.wrapped) - w2.rotate(p1,p2,angleDegrees) + w2.rotate(p1, p2, angleDegrees) endWires.append(w2) - #make a ruled surface for each set of wires + # make a ruled surface for each set of wires sides = [] - for w1,w2 in zip(startWires,endWires): - rs = FreeCADPart.makeRuledSurface(w1,w2) + for w1, w2 in zip(startWires, endWires): + rs = FreeCADPart.makeRuledSurface(w1, w2) sides.append(rs) #make faces for the top and bottom @@ -689,7 +696,7 @@ class Solid(Shape): endFace = FreeCADPart.Face(endWires) #collect all the faces from the sides - faceList = [ startFace] + faceList = [startFace] for s in sides: faceList.extend(s.Faces) faceList.append(endFace) @@ -699,7 +706,7 @@ class Solid(Shape): return Shape.cast(solid) @classmethod - def extrudeLinear(cls,outerWire,innerWires,vecNormal): + def extrudeLinear(cls, outerWire, innerWires, vecNormal): """ Attempt to extrude the list of wires into a prismatic solid in the provided direction @@ -722,9 +729,9 @@ class Solid(Shape): 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. + # 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 @@ -737,24 +744,68 @@ class Solid(Shape): return Shape.cast(result) - def tessellate(self,tolerance): + @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): + 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): + def cut(self, solidToCut): "Remove a solid from another one" return Shape.cast(self.wrapped.cut(solidToCut.wrapped)) - def fuse(self,solidToJoin): + def fuse(self, solidToJoin): return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped)) - def fillet(self,radius,edgeList): + def fillet(self, radius, edgeList): """ Fillets the specified edges of this solid. :param radius: float > 0, the radius of the fillet @@ -762,9 +813,9 @@ class Solid(Shape): :return: Filleted solid """ nativeEdges = [e.wrapped for e in edgeList] - return Shape.cast(self.wrapped.makeFillet(radius,nativeEdges)) + return Shape.cast(self.wrapped.makeFillet(radius, nativeEdges)) - def shell(self,faceList,thickness,tolerance=0.0001): + def shell(self, faceList, thickness, tolerance=0.0001): """ make a shelled solid of given by removing the list of faces @@ -776,31 +827,32 @@ class Solid(Shape): **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)) + nativeFaces = [f.wrapped for f in faceList] + return Shape.cast(self.wrapped.makeThickness(nativeFaces, thickness, tolerance)) + class Compound(Shape): - def __init__(self,obj): + def __init__(self, obj): """ An Edge """ self.wrapped = obj def Center(self): - #TODO: compute the weighted average instead of the first solid + # TODO: compute the weighted average instead of the first solid return self.Solids()[0].Center() @classmethod - def makeCompound(cls,listOfShapes): + 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) + return Shape.cast(c) - def fuse(self,toJoin): + def fuse(self, toJoin): return Shape.cast(self.wrapped.fuse(toJoin.wrapped)) - def tessellate(self,tolerance): + def tessellate(self, tolerance): return self.wrapped.tessellate(tolerance) diff --git a/examples/FreeCAD/Ex025_Revolution.py b/examples/FreeCAD/Ex025_Revolution.py new file mode 100644 index 0000000..7b17422 --- /dev/null +++ b/examples/FreeCAD/Ex025_Revolution.py @@ -0,0 +1,44 @@ +#File: Ex025_Revolution.py +#To use this example file, you need to first follow the "Using CadQuery From Inside FreeCAD" +#instructions here: https://github.com/dcowden/cadquery#installing----using-cadquery-from-inside-freecad + +#You run this example by typing the following in the FreeCAD python console, making sure to change +#the path to this example, and the name of the example appropriately. +#import sys +#sys.path.append('/home/user/Downloads/cadquery/examples/FreeCAD') +#import Ex025_Revolution + +#If you need to reload the part after making a change, you can use the following lines within the FreeCAD console. +#reload(Ex025_Revolution) + +#You'll need to delete the original shape that was created, and the new shape should be named sequentially (Shape001, etc). + +#You can also tie these blocks of code to macros, buttons, and keybindings in FreeCAD for quicker access. +#You can get a more information on this example at http://parametricparts.com/docs/examples.html#an-extruded-prismatic-solid + +import cadquery +import Part + +#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 + +#Revolve a cylinder from a rectangle +#Switch comments around in this section to try the revolve operation with different parameters +result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) + +#Revolve a donut with square walls +#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10)) + +#Get a cadquery solid object +solid = result.val() + +#Use the wrapped property of a cadquery primitive to get a FreeCAD solid +Part.show(solid.wrapped) + +#Would like to zoom to fit the part here, but FreeCAD doesn't seem to have that scripting functionality diff --git a/setup.py b/setup.py index 40de6e5..b8f5ad1 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name='cadquery', - version='0.1.6', + version='0.1.7', url='https://github.com/dcowden/cadquery', license='LGPL', author='David Cowden', diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 4f70e2a..054bd6a 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -219,6 +219,87 @@ class TestCadQuery(BaseTest): #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 testRectArray(self): NUMX=3 NUMY=3