Merge pull request #43 from jmwright/master

Suuuhhhh Weet!  thanks for contributing this feature!
This commit is contained in:
Dave Cowden 2014-10-21 18:23:26 -04:00
commit 8041accf9f
8 changed files with 405 additions and 149 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
*.pyc *.pyc
doc/_build/* doc/_build/*
dist/* dist/*
.idea/*

View File

@ -1,6 +1,6 @@
Metadata-Version: 1.0 Metadata-Version: 1.1
Name: cadquery Name: cadquery
Version: 0.1.5 Version: 0.1.6
Summary: CadQuery is a parametric scripting language for creating and traversing CAD models Summary: CadQuery is a parametric scripting language for creating and traversing CAD models
Home-page: https://github.com/dcowden/cadquery Home-page: https://github.com/dcowden/cadquery
Author: David Cowden Author: David Cowden

View File

@ -16,6 +16,7 @@ cadquery/contrib/__init__.py
cadquery/freecad_impl/__init__.py cadquery/freecad_impl/__init__.py
cadquery/freecad_impl/exporters.py cadquery/freecad_impl/exporters.py
cadquery/freecad_impl/geom.py cadquery/freecad_impl/geom.py
cadquery/freecad_impl/importers.py
cadquery/freecad_impl/shapes.py cadquery/freecad_impl/shapes.py
cadquery/freecad_impl/verutil.py cadquery/freecad_impl/verutil.py
cadquery/plugins/__init__.py cadquery/plugins/__init__.py
@ -23,6 +24,7 @@ tests/TestCQSelectors.py
tests/TestCadObjects.py tests/TestCadObjects.py
tests/TestCadQuery.py tests/TestCadQuery.py
tests/TestExporters.py tests/TestExporters.py
tests/TestImporters.py
tests/TestImports.py tests/TestImports.py
tests/TestWorkplanes.py tests/TestWorkplanes.py
tests/__init__.py tests/__init__.py

View File

@ -1868,6 +1868,55 @@ class Workplane(CQ):
else: else:
return self.newObject([r]) 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): def _combineWithBase2(self,obj):
""" """
Combines the provided object with the base solid, if one can be found. Combines the provided object with the base solid, if one can be found.
@ -2105,6 +2154,33 @@ class Workplane(CQ):
return Compound.makeCompound(toFuse) 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): def box(self,length,width,height,centered=(True,True,True),combine=True):
""" """

View File

@ -47,45 +47,47 @@
object each one returns, so these are better grouped by the type of object they return. 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 ? (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 import FreeCAD
from .verutil import fc_import from .verutil import fc_import
FreeCADPart = fc_import("FreeCAD.Part") FreeCADPart = fc_import("FreeCAD.Part")
class Shape(object): class Shape(object):
""" """
Represents a shape in the system. Represents a shape in the system.
Wrappers the FreeCAD api Wrappers the FreeCAD api
""" """
def __init__(self,obj): def __init__(self, obj):
self.wrapped = obj self.wrapped = obj
self.forConstruction = False self.forConstruction = False
@classmethod @classmethod
def cast(cls,obj,forConstruction = False): def cast(cls, obj, forConstruction=False):
"Returns the right type of wrapper, given a FreeCAD object" "Returns the right type of wrapper, given a FreeCAD object"
s = obj.ShapeType s = obj.ShapeType
if type(obj) == FreeCAD.Base.Vector: if type(obj) == FreeCAD.Base.Vector:
return Vector(obj) return Vector(obj)
tr = None tr = None
#TODO: there is a clever way to do this i'm sure with a lookup # 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 # but it is not a perfect mapping, because we are trying to hide
#a bit of the complexity of Compounds in FreeCAD. # a bit of the complexity of Compounds in FreeCAD.
if s == 'Vertex': if s == 'Vertex':
tr= Vertex(obj) tr = Vertex(obj)
elif s == 'Edge': elif s == 'Edge':
tr= Edge(obj) tr = Edge(obj)
elif s == 'Wire': elif s == 'Wire':
tr = Wire(obj) tr = Wire(obj)
elif s == 'Face': elif s == 'Face':
tr= Face(obj) tr = Face(obj)
elif s == 'Shell': elif s == 'Shell':
tr= Shell(obj) tr = Shell(obj)
elif s == 'Solid': elif s == 'Solid':
tr= Solid(obj) tr = Solid(obj)
elif s == 'Compound': elif s == 'Compound':
#compound of solids, lets return a solid instead #compound of solids, lets return a solid instead
if len(obj.Solids) > 1: if len(obj.Solids) > 1:
@ -95,22 +97,23 @@ class Shape(object):
elif len(obj.Wires) > 0: elif len(obj.Wires) > 0:
tr = Wire(obj) tr = Wire(obj)
else: else:
tr= Compound(obj) tr = Compound(obj)
else: else:
raise ValueError("cast:unknown shape type %s" % s) raise ValueError("cast:unknown shape type %s" % s)
tr.forConstruction = forConstruction tr.forConstruction = forConstruction
return tr return tr
#TODO: all these should move into the exporters folder.
#we dont need a bunch of exporting code stored in here! # TODO: all these should move into the exporters folder.
# we dont need a bunch of exporting code stored in here!
# #
def exportStl(self,fileName): def exportStl(self, fileName):
self.wrapped.exportStl(fileName) self.wrapped.exportStl(fileName)
def exportStep(self,fileName): def exportStep(self, fileName):
self.wrapped.exportStep(fileName) self.wrapped.exportStep(fileName)
def exportShape(self,fileName, fileFormat): def exportShape(self, fileName, fileFormat):
if fileFormat == ExportFormats.STL: if fileFormat == ExportFormats.STL:
self.wrapped.exportStl(fileName) self.wrapped.exportStl(fileName)
elif fileFormat == ExportFormats.BREP: elif fileFormat == ExportFormats.BREP:
@ -118,7 +121,7 @@ class Shape(object):
elif fileFormat == ExportFormats.STEP: elif fileFormat == ExportFormats.STEP:
self.wrapped.exportStep(fileName) self.wrapped.exportStep(fileName)
elif fileFormat == ExportFormats.AMF: elif fileFormat == ExportFormats.AMF:
#not built into FreeCAD # not built into FreeCAD
#TODO: user selected tolerance #TODO: user selected tolerance
tess = self.wrapped.tessellate(0.1) tess = self.wrapped.tessellate(0.1)
aw = amfUtils.AMFWriter(tess) aw = amfUtils.AMFWriter(tess)
@ -154,14 +157,14 @@ class Shape(object):
""" """
return self.wrapped.ShapeType 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 Returns True if the shape is the specified type, false otherwise
contrast with ShapeType, which will raise an exception contrast with ShapeType, which will raise an exception
if the provide object is not a shape at all if the provide object is not a shape at all
""" """
if hasattr(obj,'ShapeType'): if hasattr(obj, 'ShapeType'):
return obj.ShapeType == strType return obj.ShapeType == strType
else: else:
return False return False
@ -172,10 +175,10 @@ class Shape(object):
def isNull(self): def isNull(self):
return self.wrapped.isNull() return self.wrapped.isNull()
def isSame(self,other): def isSame(self, other):
return self.wrapped.isSame(other.wrapped) return self.wrapped.isSame(other.wrapped)
def isEqual(self,other): def isEqual(self, other):
return self.wrapped.isEqual(other.wrapped) return self.wrapped.isEqual(other.wrapped)
def isValid(self): def isValid(self):
@ -189,6 +192,7 @@ class Shape(object):
return Vector(self.wrapped.CenterOfMass) return Vector(self.wrapped.CenterOfMass)
except: except:
pass pass
def Closed(self): def Closed(self):
return self.wrapped.Closed return self.wrapped.Closed
@ -222,7 +226,7 @@ class Shape(object):
def Length(self): def Length(self):
return self.wrapped.Length return self.wrapped.Length
def rotate(self,startVector,endVector,angleDegrees): def rotate(self, startVector, endVector, angleDegrees):
""" """
Rotates a shape around an axis Rotates a shape around an axis
:param startVector: start point of rotation axis either a 3-tuple or a Vector :param startVector: start point of rotation axis either a 3-tuple or a Vector
@ -237,10 +241,10 @@ class Shape(object):
endVector = Vector(endVector) endVector = Vector(endVector)
tmp = self.wrapped.copy() tmp = self.wrapped.copy()
tmp.rotate(startVector.wrapped,endVector.wrapped,angleDegrees) tmp.rotate(startVector.wrapped, endVector.wrapped, angleDegrees)
return Shape.cast(tmp) return Shape.cast(tmp)
def translate(self,vector): def translate(self, vector):
if type(vector) == tuple: if type(vector) == tuple:
vector = Vector(vector) vector = Vector(vector)
@ -248,7 +252,7 @@ class Shape(object):
tmp.translate(vector.wrapped) tmp.translate(vector.wrapped)
return Shape.cast(tmp) return Shape.cast(tmp)
def scale(self,factor): def scale(self, factor):
tmp = self.wrapped.copy() tmp = self.wrapped.copy()
tmp.scale(factor) tmp.scale(factor)
return Shape.cast(tmp) return Shape.cast(tmp)
@ -256,7 +260,7 @@ class Shape(object):
def copy(self): def copy(self):
return Shape.cast(self.wrapped.copy()) 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, returns a copy of the ojbect, transformed by the provided matrix,
@ -268,7 +272,7 @@ class Shape(object):
r.forConstruction = self.forConstruction r.forConstruction = self.forConstruction
return r return r
def transformGeometry(self,tMatrix): def transformGeometry(self, tMatrix):
""" """
tMatrix is a matrix object. tMatrix is a matrix object.
@ -288,8 +292,9 @@ class Shape(object):
def __hash__(self): def __hash__(self):
return self.wrapped.hashCode() return self.wrapped.hashCode()
class Vertex(Shape): class Vertex(Shape):
def __init__(self,obj,forConstruction=False): def __init__(self, obj, forConstruction=False):
""" """
Create a vertex from a FreeCAD Vertex Create a vertex from a FreeCAD Vertex
""" """
@ -300,7 +305,7 @@ class Vertex(Shape):
self.Z = obj.Z self.Z = obj.Z
def toTuple(self): def toTuple(self):
return (self.X,self.Y,self.Z) return (self.X, self.Y, self.Z)
def Center(self): def Center(self):
""" """
@ -308,19 +313,20 @@ class Vertex(Shape):
""" """
return Vector(self.wrapped.Point) return Vector(self.wrapped.Point)
class Edge(Shape): class Edge(Shape):
def __init__(self,obj): def __init__(self, obj):
""" """
An Edge An Edge
""" """
self.wrapped = obj self.wrapped = obj
#self.startPoint = None # self.startPoint = None
#self.endPoint = None # self.endPoint = None
self.edgetypes= { self.edgetypes = {
FreeCADPart.Line : 'LINE', FreeCADPart.Line: 'LINE',
FreeCADPart.ArcOfCircle : 'ARC', FreeCADPart.ArcOfCircle: 'ARC',
FreeCADPart.Circle : 'CIRCLE' FreeCADPart.Circle: 'CIRCLE'
} }
def geomType(self): def geomType(self):
@ -337,9 +343,9 @@ class Edge(Shape):
Note, circles may have the start and end points the same 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 curve = self.wrapped.Curve
return Vector( curve.value(self.wrapped.ParameterRange[0])) return Vector(curve.value(self.wrapped.ParameterRange[0]))
def endPoint(self): def endPoint(self):
""" """
@ -349,14 +355,14 @@ class Edge(Shape):
Note, circles may have the start and end points the same Note, circles may have the start and end points the same
""" """
#warning: easier syntax in freecad of <Edge>.valueAt(<Edge>.ParameterRange[1]) has # warning: easier syntax in freecad of <Edge>.valueAt(<Edge>.ParameterRange[1]) has
#a bug with curves other than arcs, but using the underlying curve directly seems to work # a bug with curves other than arcs, but using the underlying curve directly seems to work
#that's the solution i'm using below # that's the solution i'm using below
curve = self.wrapped.Curve curve = self.wrapped.Curve
v = Vector( curve.value(self.wrapped.ParameterRange[1])) v = Vector(curve.value(self.wrapped.ParameterRange[1]))
return v return v
def tangentAt(self,locationVector=None): def tangentAt(self, locationVector=None):
""" """
Compute tangent vector at the specified location. Compute tangent vector at the specified location.
:param locationVector: location to use. Use the center point if None :param locationVector: location to use. Use the center point if None
@ -369,11 +375,11 @@ class Edge(Shape):
return Vector(self.wrapped.tangentAt(p)) return Vector(self.wrapped.tangentAt(p))
@classmethod @classmethod
def makeCircle(cls,radius,pnt=(0,0,0),dir=(0,0,1),angle1=360.0,angle2=360): 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)) return Edge(FreeCADPart.makeCircle(radius, toVector(pnt), toVector(dir), angle1, angle2))
@classmethod @classmethod
def makeSpline(cls,listOfVector): def makeSpline(cls, listOfVector):
""" """
Interpolate a spline through the provided points. Interpolate a spline through the provided points.
:param cls: :param cls:
@ -383,11 +389,11 @@ class Edge(Shape):
vecs = [v.wrapped for v in listOfVector] vecs = [v.wrapped for v in listOfVector]
spline = FreeCADPart.BSplineCurve() spline = FreeCADPart.BSplineCurve()
spline.interpolate(vecs,False) spline.interpolate(vecs, False)
return Edge(spline.toShape()) return Edge(spline.toShape())
@classmethod @classmethod
def makeThreePointArc(cls,v1,v2,v3): def makeThreePointArc(cls, v1, v2, v3):
""" """
Makes a three point arc through the provided points Makes a three point arc through the provided points
:param cls: :param cls:
@ -396,30 +402,30 @@ class Edge(Shape):
:param v3: end vector :param v3: end vector
:return: an edge object through the three points :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()) 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 @classmethod
def makeLine(cls,v1,v2): def makeLine(cls, v1, v2):
""" """
Create a line between two points Create a line between two points
:param v1: Vector that represents the first point :param v1: Vector that represents the first point
:param v2: Vector that represents the second point :param v2: Vector that represents the second point
:return: A linear edge between the two provided points :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): class Wire(Shape):
def __init__(self,obj): def __init__(self, obj):
""" """
A Wire A Wire
""" """
self.wrapped = obj self.wrapped = obj
@classmethod @classmethod
def combine(cls,listOfWires): def combine(cls, listOfWires):
""" """
Attempt to combine a list of wires into a new wire. Attempt to combine a list of wires into a new wire.
the wires are returned in a list. 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])) return Shape.cast(FreeCADPart.Wire([w.wrapped for w in listOfWires]))
@classmethod @classmethod
def assembleEdges(cls,listOfEdges): def assembleEdges(cls, listOfEdges):
""" """
Attempts to build a wire that consists of the edges in the provided list Attempts to build a wire that consists of the edges in the provided list
:param cls: :param cls:
@ -439,11 +445,11 @@ class Wire(Shape):
""" """
fCEdges = [a.wrapped for a in listOfEdges] fCEdges = [a.wrapped for a in listOfEdges]
wa = Wire( FreeCADPart.Wire(fCEdges) ) wa = Wire(FreeCADPart.Wire(fCEdges))
return wa return wa
@classmethod @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 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 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 :param normal: vector representing the direction of the plane the circle should lie in
:return: :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 return w
@classmethod @classmethod
def makePolygon(cls,listOfVertices,forConstruction=False): def makePolygon(cls, listOfVertices, forConstruction=False):
#convert list of tuples into Vectors. # convert list of tuples into Vectors.
w = Wire(FreeCADPart.makePolygon([i.wrapped for i in listOfVertices])) w = Wire(FreeCADPart.makePolygon([i.wrapped for i in listOfVertices]))
w.forConstruction = forConstruction w.forConstruction = forConstruction
return w return w
@classmethod @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 Make a helix with a given pitch, height and radius
By default a cylindrical surface is used to create the helix. If 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' 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): class Face(Shape):
def __init__(self,obj): def __init__(self, obj):
""" """
A Face A Face
""" """
self.wrapped = obj self.wrapped = obj
self.facetypes = { self.facetypes = {
#TODO: bezier,bspline etc # TODO: bezier,bspline etc
FreeCADPart.Plane : 'PLANE', FreeCADPart.Plane: 'PLANE',
FreeCADPart.Sphere : 'SPHERE', FreeCADPart.Sphere: 'SPHERE',
FreeCADPart.Cone : 'CONE' FreeCADPart.Cone: 'CONE'
} }
def geomType(self): def geomType(self):
@ -492,7 +498,7 @@ class Face(Shape):
else: else:
return "Unknown Face Surface Type: %s" % str(t) 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. Computes the normal vector at the desired location on the face.
@ -502,31 +508,31 @@ class Face(Shape):
""" """
if locationVector == None: if locationVector == None:
locationVector = self.Center() 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 @classmethod
def makePlane(cls,length,width,basePnt=None,dir=None): def makePlane(cls, length, width, basePnt=None, dir=None):
return Face(FreeCADPart.makePlan(length,width,toVector(basePnt),toVector(dir))) return Face(FreeCADPart.makePlan(length, width, toVector(basePnt), toVector(dir)))
@classmethod @classmethod
def makeRuledSurface(cls,edgeOrWire1,edgeOrWire2,dist=None): def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None):
""" """
'makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface 'makeRuledSurface(Edge|Wire,Edge|Wire) -- Make a ruled surface
Create a ruled surface out of two edges or wires. If wires are used then Create a ruled surface out of two edges or wires. If wires are used then
these must have the same 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" "Remove a face from another one"
return Shape.cast(self.obj.cut(faceToCut.obj)) return Shape.cast(self.obj.cut(faceToCut.obj))
def fuse(self,faceToJoin): def fuse(self, faceToJoin):
return Shape.cast(self.obj.fuse(faceToJoin.obj)) 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. computes the intersection between the face and the supplied one.
The result could be a face or a compound of faces The result could be a face or a compound of faces
@ -535,73 +541,73 @@ class Face(Shape):
class Shell(Shape): class Shell(Shape):
def __init__(self,wrapped): def __init__(self, wrapped):
""" """
A Shell A Shell
""" """
self.wrapped = wrapped self.wrapped = wrapped
@classmethod @classmethod
def makeShell(cls,listOfFaces): def makeShell(cls, listOfFaces):
return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces])) return Shell(FreeCADPart.makeShell([i.obj for i in listOfFaces]))
class Solid(Shape): class Solid(Shape):
def __init__(self,obj): def __init__(self, obj):
""" """
A Solid A Solid
""" """
self.wrapped = obj self.wrapped = obj
@classmethod @classmethod
def isSolid(cls,obj): def isSolid(cls, obj):
""" """
Returns true if the object is a FreeCAD solid, false otherwise Returns true if the object is a FreeCAD solid, false otherwise
""" """
if hasattr(obj, 'ShapeType'): if hasattr(obj, 'ShapeType'):
if obj.ShapeType == 'Solid' or\ if obj.ShapeType == 'Solid' or \
(obj.ShapeType == 'Compound' and len(obj.Solids) > 0): (obj.ShapeType == 'Compound' and len(obj.Solids) > 0):
return True return True
return False return False
@classmethod @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 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)' 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 @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]) -- 'makeCone(radius1,radius2,height,[pnt,dir,angle]) --
Make a cone with given radii and height\nBy default pnt=Vector(0,0,0), Make a cone with given radii and height\nBy default pnt=Vector(0,0,0),
dir=Vector(0,0,1) and angle=360' 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 @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]) -- makeCylinder(radius,height,[pnt,dir,angle]) --
Make a cylinder with a given radius and height Make a cylinder with a given radius and height
By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360' 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 @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]) -- makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) --
Make a torus with agiven radii and angles Make a torus with agiven radii and angles
By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0 By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0
,angle1=360 and angle=360' ,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 @classmethod
def sweep(cls,profileWire,pathWire): def sweep(cls, profileWire, pathWire):
""" """
make a solid by sweeping the profileWire along the specified path make a solid by sweeping the profileWire along the specified path
:param cls: :param cls:
@ -609,41 +615,42 @@ class Solid(Shape):
:param pathWire: :param pathWire:
:return: :return:
""" """
#needs to use freecad wire.makePipe or makePipeShell # needs to use freecad wire.makePipe or makePipeShell
#needs to allow free-space wires ( those not made from a workplane ) # needs to allow free-space wires ( those not made from a workplane )
@classmethod @classmethod
def makeLoft(cls,listOfWire): def makeLoft(cls, listOfWire):
""" """
makes a loft from a list of wires makes a loft from a list of wires
The wires will be converted into faces when possible-- it is presumed that nobody ever actually 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. 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 @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, 'makeWedge(xmin, ymin, zmin, z2min, x2min,
xmax, ymax, zmax, z2max, x2max,[pnt, dir]) xmax, ymax, zmax, z2max, x2max,[pnt, dir])
Make a wedge located in pnt\nBy default pnt=Vector(0,0,0) and dir=Vec Make a wedge located in pnt\nBy default pnt=Vector(0,0,0) and dir=Vec
tor(0,0,1)' 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 @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]) -- 'makeSphere(radius,[pnt, dir, angle1,angle2,angle3]) --
Make a sphere with a giv 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' 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 @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. 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 :return: a cad.Solid object
""" """
#from this point down we are dealing with FreeCAD wires not cad.wires # from this point down we are dealing with FreeCAD wires not cad.wires
startWires = [outerWire.wrapped] + [ i.wrapped for i in innerWires] startWires = [outerWire.wrapped] + [i.wrapped for i in innerWires]
endWires = [] endWires = []
p1 = vecCenter.wrapped p1 = vecCenter.wrapped
p2 = vecCenter.add(vecNormal).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: for w in startWires:
w2 = w.copy() w2 = w.copy()
w2.translate(vecNormal.wrapped) w2.translate(vecNormal.wrapped)
w2.rotate(p1,p2,angleDegrees) w2.rotate(p1, p2, angleDegrees)
endWires.append(w2) endWires.append(w2)
#make a ruled surface for each set of wires # make a ruled surface for each set of wires
sides = [] sides = []
for w1,w2 in zip(startWires,endWires): for w1, w2 in zip(startWires, endWires):
rs = FreeCADPart.makeRuledSurface(w1,w2) rs = FreeCADPart.makeRuledSurface(w1, w2)
sides.append(rs) sides.append(rs)
#make faces for the top and bottom #make faces for the top and bottom
@ -689,7 +696,7 @@ class Solid(Shape):
endFace = FreeCADPart.Face(endWires) endFace = FreeCADPart.Face(endWires)
#collect all the faces from the sides #collect all the faces from the sides
faceList = [ startFace] faceList = [startFace]
for s in sides: for s in sides:
faceList.extend(s.Faces) faceList.extend(s.Faces)
faceList.append(endFace) faceList.append(endFace)
@ -699,7 +706,7 @@ class Solid(Shape):
return Shape.cast(solid) return Shape.cast(solid)
@classmethod @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 Attempt to extrude the list of wires into a prismatic solid in the provided direction
@ -722,9 +729,9 @@ class Solid(Shape):
reliable. reliable.
""" """
#one would think that fusing faces into a compound and then extruding would work, # 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 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. # 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 #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 #FreeCAD allows this in one operation, but others might not
@ -737,24 +744,68 @@ class Solid(Shape):
return Shape.cast(result) 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) return self.wrapped.tessellate(tolerance)
def intersect(self,toIntersect): def intersect(self, toIntersect):
""" """
computes the intersection between this solid and the supplied one computes the intersection between this solid and the supplied one
The result could be a face or a compound of faces The result could be a face or a compound of faces
""" """
return Shape.cast(self.wrapped.common(toIntersect.wrapped)) return Shape.cast(self.wrapped.common(toIntersect.wrapped))
def cut(self,solidToCut): def cut(self, solidToCut):
"Remove a solid from another one" "Remove a solid from another one"
return Shape.cast(self.wrapped.cut(solidToCut.wrapped)) return Shape.cast(self.wrapped.cut(solidToCut.wrapped))
def fuse(self,solidToJoin): def fuse(self, solidToJoin):
return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped)) 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. Fillets the specified edges of this solid.
:param radius: float > 0, the radius of the fillet :param radius: float > 0, the radius of the fillet
@ -762,9 +813,9 @@ class Solid(Shape):
:return: Filleted solid :return: Filleted solid
""" """
nativeEdges = [e.wrapped for e in edgeList] 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 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 **WARNING** The underlying FreeCAD implementation can very frequently have problems
with shelling complex geometries! with shelling complex geometries!
""" """
nativeFaces = [ f.wrapped for f in faceList] nativeFaces = [f.wrapped for f in faceList]
return Shape.cast( self.wrapped.makeThickness(nativeFaces,thickness,tolerance)) return Shape.cast(self.wrapped.makeThickness(nativeFaces, thickness, tolerance))
class Compound(Shape): class Compound(Shape):
def __init__(self,obj): def __init__(self, obj):
""" """
An Edge An Edge
""" """
self.wrapped = obj self.wrapped = obj
def Center(self): 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() return self.Solids()[0].Center()
@classmethod @classmethod
def makeCompound(cls,listOfShapes): def makeCompound(cls, listOfShapes):
""" """
Create a compound out of a list of shapes Create a compound out of a list of shapes
""" """
solids = [s.wrapped for s in listOfShapes] solids = [s.wrapped for s in listOfShapes]
c = FreeCADPart.Compound(solids) 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)) return Shape.cast(self.wrapped.fuse(toJoin.wrapped))
def tessellate(self,tolerance): def tessellate(self, tolerance):
return self.wrapped.tessellate(tolerance) return self.wrapped.tessellate(tolerance)

View File

@ -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

View File

@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name='cadquery', name='cadquery',
version='0.1.6', version='0.1.7',
url='https://github.com/dcowden/cadquery', url='https://github.com/dcowden/cadquery',
license='LGPL', license='LGPL',
author='David Cowden', author='David Cowden',

View File

@ -219,6 +219,87 @@ class TestCadQuery(BaseTest):
#self.assertEqual(1,s.solids().size() ) #self.assertEqual(1,s.solids().size() )
#self.assertEqual(8,s.faces().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): def testRectArray(self):
NUMX=3 NUMX=3
NUMY=3 NUMY=3