initial try at the layout
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.pyc
|
||||
sphinxdoc/_build/*
|
753
cadquery/CQ.py
Normal file
@ -0,0 +1,753 @@
|
||||
"""
|
||||
A Parametric CAD System for the Web
|
||||
Copyright (c) 2010-2022 Parametric Products Intellectual Holdings LLC, All Rights Reserved.
|
||||
|
||||
The Core CadQuery object
|
||||
"""
|
||||
|
||||
class CQContext(object):
|
||||
"""
|
||||
A shared context for modeling.
|
||||
|
||||
All objects in the same CQ chain share a reference to this same object instance
|
||||
which allows for shared state when needed,
|
||||
"""
|
||||
def __init__(self):
|
||||
self.pendingWires = [] #a list of wires that have been created and need to be extruded
|
||||
self.pendingEdges = [] #a list of pending edges that have been created and need to be joined into wires
|
||||
self.firstPoint = None #a reference to the first point for a set of edges. used to determine how to behave when close() is called
|
||||
self.tolerance = 0.0001 #user specified tolerance
|
||||
|
||||
|
||||
class CQ(object):
|
||||
"""
|
||||
Provides enhanced functionality for a wrapped CAD primitive.
|
||||
|
||||
Examples include feature selection, feature creation, 2d drawing
|
||||
using work planes, and 3d opertations like fillets, shells, and splitting
|
||||
"""
|
||||
|
||||
def __init__(self,obj):
|
||||
"""
|
||||
Construct a new cadquery (CQ) object that wraps a CAD primitive.
|
||||
|
||||
:param obj: Object to Wrap.
|
||||
:type obj: A CAD Primitive ( wire,vertex,face,solid,edge )
|
||||
"""
|
||||
self.objects = []
|
||||
self.ctx = CQContext()
|
||||
self.parent = None
|
||||
|
||||
if obj: #guarded because sometimes None for internal use
|
||||
self.objects.append(obj)
|
||||
|
||||
def newObject(self,objlist):
|
||||
"""
|
||||
Make a new CQ object.
|
||||
|
||||
:param objlist: The stack of objects to use
|
||||
:param newContextSolid: an optional new solid to become the new context solid
|
||||
|
||||
:type objlist: a list of CAD primitives ( wire,face,edge,solid,vertex,etc )
|
||||
|
||||
The parent of the new object will be set to the current object,
|
||||
to preserve the chain correctly.
|
||||
|
||||
Custom plugins and subclasses should use this method to create new CQ objects
|
||||
correctly.
|
||||
"""
|
||||
r = CQ(None) #create a completely blank one
|
||||
r.parent = self
|
||||
r.ctx = self.ctx #context solid remains the same
|
||||
r.objects = list(objlist)
|
||||
return r
|
||||
|
||||
def _collectProperty(self,propName):
|
||||
"""
|
||||
Collects all of the values for propName,
|
||||
for all items on the stack.
|
||||
FreeCAD objects do not implement id correclty,
|
||||
so hashCode is used to ensure we dont add the same
|
||||
object multiple times.
|
||||
|
||||
One weird use case is that the stack could have a solid reference object
|
||||
on it. This is meant to be a reference to the most recently modified version
|
||||
of the context solid, whatever it is.
|
||||
"""
|
||||
all = {}
|
||||
for o in self.objects:
|
||||
|
||||
#tricky-- if an object is a compound of solids,
|
||||
#do not return all of the solids underneath-- typically
|
||||
#then we'll keep joining to ourself
|
||||
if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() =='Compound':
|
||||
for i in getattr(o,'Compounds')():
|
||||
all[i.hashCode()] = i
|
||||
else:
|
||||
if hasattr(o,propName):
|
||||
for i in getattr(o,propName)():
|
||||
all[i.hashCode()] = i
|
||||
|
||||
return list(all.values())
|
||||
|
||||
def split(self,keepTop=False,keepBottom=False):
|
||||
"""
|
||||
Splits a solid on the stack into two parts, optionally keeping the separate parts.
|
||||
|
||||
:param boolean keepTop: True to keep the top, False or None to discard it
|
||||
:param boolean keepBottom: True to keep the bottom, False or None to discard it
|
||||
:raises: ValueError if keepTop and keepBottom are both false.
|
||||
:raises: ValueError if there is not a solid in the current stack or the parent chain
|
||||
:returns: CQ object with the desired objects on the stack.
|
||||
|
||||
The most common operation splits a solid and keeps one half. This sample creates split bushing::
|
||||
|
||||
#drill a hole in the side
|
||||
c = Workplane().box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()F
|
||||
#now cut it in half sideways
|
||||
c.faces(">Y").workplane(-0.5).split(keepTop=True)
|
||||
|
||||
"""
|
||||
|
||||
solid = self.findSolid()
|
||||
|
||||
if (not keepTop) and (not keepBottom):
|
||||
raise ValueError("You have to keep at least one half")
|
||||
|
||||
maxDim = solid.BoundingBox().DiagonalLength * 10.0
|
||||
topCutBox = self.rect(maxDim,maxDim)._extrude(maxDim)
|
||||
bottomCutBox = self.rect(maxDim,maxDim)._extrude(-maxDim)
|
||||
|
||||
top = solid.cut(bottomCutBox)
|
||||
bottom = solid.cut(topCutBox)
|
||||
|
||||
if keepTop and keepBottom:
|
||||
#put both on the stack, leave original unchanged
|
||||
return self.newObject([top,bottom])
|
||||
else:
|
||||
# put the one we are keeping on the stack, and also update the context solid
|
||||
#to the one we kept
|
||||
if keepTop:
|
||||
solid.wrapped = top.wrapped
|
||||
return self.newObject([top])
|
||||
else:
|
||||
solid.wrapped = bottom.wrapped
|
||||
return self.newObject([bottom])
|
||||
|
||||
|
||||
def combineSolids(self,otherCQToCombine=None):
|
||||
"""
|
||||
!!!DEPRECATED!!! use union()
|
||||
Combines all solids on the current stack, and any context object, together
|
||||
into a single object.
|
||||
|
||||
After the operation, the returned solid is also the context solid.
|
||||
|
||||
:param otherCQToCombine: another cadquery to combine.
|
||||
:return: a cQ object with the resulting combined solid on the stack.
|
||||
|
||||
Most of the time, both objects will contain a single solid, which is
|
||||
combined and returned on the stack of the new object.
|
||||
|
||||
"""
|
||||
#loop through current stack objects, and combine them
|
||||
#TODO: combine other types of objects as well, like edges and wires
|
||||
toCombine = self.solids().vals()
|
||||
|
||||
if otherCQToCombine:
|
||||
for obj in otherCQToCombine.solids().vals():
|
||||
toCombine.append(obj)
|
||||
|
||||
if len(toCombine) < 1:
|
||||
raise ValueError("Cannot Combine: at least one solid required!")
|
||||
|
||||
#get context solid
|
||||
ctxSolid = self.findSolid(searchStack=False,searchParents=True) #we dont want to find our own objects
|
||||
|
||||
if ctxSolid is None:
|
||||
ctxSolid = toCombine.pop(0)
|
||||
|
||||
#now combine them all. make sure to save a reference to the ctxSolid pointer!
|
||||
s = ctxSolid
|
||||
for tc in toCombine:
|
||||
s = s.fuse(tc)
|
||||
|
||||
ctxSolid.wrapped = s.wrapped
|
||||
return self.newObject([s])
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Return a list of all CQ objects on the stack.
|
||||
|
||||
useful when you need to operate on the elements
|
||||
individually.
|
||||
|
||||
Contrast with vals, which returns the underlying
|
||||
objects for all of the items on the stack
|
||||
|
||||
"""
|
||||
return [self.newObject([o]) for o in self.objects]
|
||||
|
||||
def size(self):
|
||||
"""
|
||||
Return the number of objects currently on the stack
|
||||
|
||||
"""
|
||||
return len(self.objects)
|
||||
|
||||
def vals(self):
|
||||
"""
|
||||
get the values in the current list
|
||||
|
||||
:rtype: list of FreeCAD objects
|
||||
:returns: the values of the objects on the stack.
|
||||
|
||||
Contrast with :py:meth:`all`, which returns CQ objects for all of the items on the stack
|
||||
|
||||
"""
|
||||
res = []
|
||||
return self.objects
|
||||
|
||||
def add(self,obj):
|
||||
"""
|
||||
adds an object or a list of objects to the stack
|
||||
|
||||
|
||||
:param obj: an object to add
|
||||
:type obj: a CQ object, CAD primitive, or list of CAD primitives
|
||||
:return: a CQ object with the requested operation performed
|
||||
|
||||
If an CQ object, the values of that object's stack are added. If a list of cad primitives,
|
||||
they are all added. If a single CAD primitive it is added
|
||||
|
||||
Used in rare cases when you need to combine the results of several CQ results
|
||||
into a single CQ object. Shelling is one common example
|
||||
|
||||
"""
|
||||
if type(obj) == list:
|
||||
self.objects.extend(obj)
|
||||
elif type(obj) == CQ or type(obj) == Workplane:
|
||||
self.objects.extend(obj.objects)
|
||||
else:
|
||||
self.objects.append(obj)
|
||||
return self
|
||||
|
||||
def val(self):
|
||||
"""
|
||||
Return the first value on the stack
|
||||
|
||||
:return: the first value on the stack.
|
||||
:rtype: A FreeCAD object or a SolidReference
|
||||
"""
|
||||
return self.objects[0]
|
||||
|
||||
|
||||
|
||||
def workplane(self,offset=0.0,invert=False):
|
||||
"""
|
||||
|
||||
Creates a new 2-D workplane, located relative to the first face on the stack.
|
||||
|
||||
:param offset: offset for the work plane in the Z direction. Default
|
||||
:param invert: invert the Z direction from that of the face.
|
||||
:type offset: float or None=0.0
|
||||
:type invert: boolean or None=False
|
||||
:rtype: Workplane object ( which is a subclass of CQ )
|
||||
|
||||
The first element on the stack must be a face, or a vertex. If a vertex, then the parent item on the
|
||||
chain immediately before the vertex must be a face.
|
||||
|
||||
The result will be a 2-d working plane
|
||||
with a new coordinate system set up as follows:
|
||||
|
||||
* The origin will be located in the *center* of the face, if a face was selected. If a vertex was
|
||||
selected, the origin will be at the vertex, and located on the face.
|
||||
* The Z direction will be normal to the plane of the face,computed
|
||||
at the center point.
|
||||
* The X direction will be parallel to the x-y plane. If the workplane is parallel to the global
|
||||
x-y plane, the x direction of the workplane will co-incide with the global x direction.
|
||||
|
||||
Most commonly, the selected face will be planar, and the workplane lies in the same plane
|
||||
of the face ( IE, offset=0). Occasionally, it is useful to define a face offset from
|
||||
an existing surface, and even more rarely to define a workplane based on a face that is not planar.
|
||||
|
||||
To create a workplane without first having a face, use the Workplane() method.
|
||||
|
||||
Future Enhancements:
|
||||
* Allow creating workplane from planar wires
|
||||
* Allow creating workplane based on an arbitrary point on a face, not just the center.
|
||||
For now you can work around by creating a workplane and then offsetting the center afterwards.
|
||||
|
||||
"""
|
||||
obj = self.objects[0]
|
||||
|
||||
def _computeXdir(normal):
|
||||
xd = Vector(0,0,1).cross(normal)
|
||||
if xd.Length < self.ctx.tolerance:
|
||||
#this face is parallel with the x-y plane, so choose x to be in global coordinates
|
||||
xd = Vector(1,0,0)
|
||||
return xd
|
||||
|
||||
faceToBuildOn = None
|
||||
center = None
|
||||
#if isinstance(obj,Vertex):
|
||||
# f = self.parent.objects[0]
|
||||
# if f != None and isinstance(f,Face):
|
||||
# center = obj.Center()
|
||||
# normal = f.normalAt(center)
|
||||
# xDir = _computeXdir(normal)
|
||||
# else:
|
||||
# raise ValueError("If a vertex is selected, a face must be the immediate parent")
|
||||
if isinstance(obj,Face):
|
||||
faceToBuildOn = obj
|
||||
center = obj.Center()
|
||||
normal = obj.normalAt(center)
|
||||
xDir = _computeXdir(normal)
|
||||
else:
|
||||
if hasattr(obj,'Center'):
|
||||
center = obj.Center()
|
||||
normal = self.plane.zDir
|
||||
xDir = self.plane.xDir
|
||||
else:
|
||||
raise ValueError ("Needs a face or a vertex or point on a work plane")
|
||||
|
||||
#invert if requested
|
||||
if invert:
|
||||
normal = normal.multiply(-1.0)
|
||||
|
||||
#offset origin if desired
|
||||
offsetVector = normal.normalize().multiply(offset)
|
||||
offsetCenter = center.add(offsetVector)
|
||||
|
||||
#make the new workplane
|
||||
plane = Plane(offsetCenter, xDir, normal)
|
||||
s = Workplane(plane)
|
||||
s.parent = self
|
||||
s.ctx = self.ctx
|
||||
|
||||
#a new workplane has the center of the workplane on the stack
|
||||
return s
|
||||
|
||||
def first(self):
|
||||
"""
|
||||
Return the first item on the stack
|
||||
:returns: the first item on the stack.
|
||||
:rtype: a CQ object
|
||||
"""
|
||||
return self.newObject(self.objects[0:1])
|
||||
|
||||
def item(self,i):
|
||||
"""
|
||||
|
||||
Return the ith item on the stack.
|
||||
:rtype: a CQ object
|
||||
"""
|
||||
return self.newObject([self.objects[i]])
|
||||
|
||||
def last(self):
|
||||
"""
|
||||
Return the last item on the stack.
|
||||
:rtype: a CQ object
|
||||
"""
|
||||
return self.newObject([self.objects[-1]])
|
||||
|
||||
def end(self):
|
||||
"""
|
||||
Return the parent of this CQ element
|
||||
:rtype: a CQ object
|
||||
:raises: ValueError if there are no more parents in the chain.
|
||||
|
||||
For example::
|
||||
|
||||
CQ(obj).faces("+Z").vertices().end()
|
||||
|
||||
will return the same as::
|
||||
|
||||
CQ(obj).faces("+Z")
|
||||
|
||||
"""
|
||||
if self.parent:
|
||||
return self.parent
|
||||
else:
|
||||
raise ValueError("Cannot End the chain-- no parents!")
|
||||
|
||||
|
||||
|
||||
def findSolid(self,searchStack=True,searchParents=True):
|
||||
"""
|
||||
Finds the first solid object in the chain, searching from the current node
|
||||
backwards through parents until one is found.
|
||||
|
||||
:param searchStack: should objects on the stack be searched first.
|
||||
:param searchParents: should parents be searched?
|
||||
:raises: ValueError if no solid is found in the current object or its parents, and errorOnEmpty is True
|
||||
|
||||
This function is very important for chains that are modifying a single parent object, most often
|
||||
a solid.
|
||||
|
||||
Most of the time, a chain defines or selects a solid, and then modifies it using workplanes
|
||||
or other operations.
|
||||
|
||||
Plugin Developers should make use of this method to find the solid that should be modified, if the
|
||||
plugin implements a unary operation, or if the operation will automatically merge its results with an
|
||||
object already on the stack.
|
||||
"""
|
||||
#notfound = ValueError("Cannot find a Valid Solid to Operate on!")
|
||||
|
||||
if searchStack:
|
||||
for s in self.objects:
|
||||
if type(s) == Solid:
|
||||
return s
|
||||
|
||||
if searchParents and self.parent is not None:
|
||||
return self.parent.findSolid(searchStack=True,searchParents=searchParents)
|
||||
|
||||
return None
|
||||
|
||||
def _selectObjects(self,objType,selector=None):
|
||||
"""
|
||||
Filters objects of the selected type with the specified selector,and returns results
|
||||
|
||||
:param objType: the type of object we are searching for
|
||||
:type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid)
|
||||
:return: a CQ object with the selected objects on the stack.
|
||||
|
||||
**Implementation Note**: This is the base implmentation of the vertices,edges,faces,solids,shells,
|
||||
and other similar selector methods. It is a useful extension point for plugin developers to make
|
||||
other selector methods.
|
||||
"""
|
||||
toReturn = self._collectProperty(objType) #all of the faces from all objects on the stack, in a single list
|
||||
|
||||
if selector is not None:
|
||||
if type(selector) == str:
|
||||
selectorObj = StringSyntaxSelector(selector)
|
||||
else:
|
||||
selectorObj = selector
|
||||
toReturn = selectorObj.filter(toReturn)
|
||||
|
||||
return self.newObject(toReturn)
|
||||
|
||||
def vertices(self,selector=None):
|
||||
"""
|
||||
Select the vertices of objects on the stack, optionally filtering the selection. If there are multiple objects
|
||||
on the stack, the vertices of all objects are collected and a list of all the distinct vertices is returned.
|
||||
|
||||
:param selector:
|
||||
:type selector: None, a Selector object, or a string selector expression.
|
||||
:return: a CQ object whos stack contains the *distinct* vertices of *all* objects on the current stack,
|
||||
after being filtered by the selector, if provided
|
||||
|
||||
If there are no vertices for any objects on the current stack, an empty CQ object is returned
|
||||
|
||||
The typical use is to select the vertices of a single object on the stack. For example::
|
||||
|
||||
Workplane().box(1,1,1).faces("+Z").vertices().size()
|
||||
|
||||
returns 4, because the topmost face of cube will contain four vertices. While this::
|
||||
|
||||
Workplane().box(1,1,1).faces().vertices().size()
|
||||
|
||||
returns 8, because a cube has a total of 8 vertices
|
||||
|
||||
**Note** Circles are peculiar, they have a single vertex at the center!
|
||||
|
||||
:py:class:`StringSyntaxSelector`
|
||||
|
||||
"""
|
||||
return self._selectObjects('Vertices',selector)
|
||||
|
||||
def faces(self,selector=None):
|
||||
"""
|
||||
Select the faces of objects on the stack, optionally filtering the selection. If there are multiple objects
|
||||
on the stack, the faces of all objects are collected and a list of all the distinct faces is returned.
|
||||
|
||||
:param selector: A selector
|
||||
:type selector: None, a Selector object, or a string selector expression.
|
||||
:return: a CQ object whos stack contains all of the *distinct* faces of *all* objects on the current stack,
|
||||
filtered by the provided selector.
|
||||
|
||||
If there are no vertices for any objects on the current stack, an empty CQ object is returned
|
||||
|
||||
The typical use is to select the faces of a single object on the stack. For example::
|
||||
|
||||
CQ(aCube).faces("+Z").size()
|
||||
|
||||
returns 1, because a cube has one face with a normal in the +Z direction. Similarly::
|
||||
|
||||
CQ(aCube).faces().size()
|
||||
|
||||
returns 6, because a cube has a total of 6 faces, And::
|
||||
|
||||
CQ(aCube).faces("|Z").size()
|
||||
|
||||
returns 2, because a cube has 2 faces having normals parallel to the z direction
|
||||
|
||||
See more about selectors HERE
|
||||
"""
|
||||
return self._selectObjects('Faces',selector)
|
||||
|
||||
def edges(self,selector=None):
|
||||
"""
|
||||
Select the edges of objects on the stack, optionally filtering the selection. If there are multiple objects
|
||||
on the stack, the edges of all objects are collected and a list of all the distinct edges is returned.
|
||||
|
||||
:param selector: A selector
|
||||
:type selector: None, a Selector object, or a string selector expression.
|
||||
:return: a CQ object whos stack contains all of the *distinct* edges of *all* objects on the current stack,
|
||||
filtered by the provided selector.
|
||||
|
||||
If there are no edges for any objects on the current stack, an empty CQ object is returned
|
||||
|
||||
The typical use is to select the edges of a single object on the stack. For example::
|
||||
|
||||
CQ(aCube).faces("+Z").edges().size()
|
||||
|
||||
returns 4, because a cube has one face with a normal in the +Z direction. Similarly::
|
||||
|
||||
CQ(aCube).edges().size()
|
||||
|
||||
returns 12, because a cube has a total of 12 edges, And::
|
||||
|
||||
CQ(aCube).edges("|Z").size()
|
||||
|
||||
returns 4, because a cube has 4 edges parallel to the z direction
|
||||
|
||||
See more about selectors HERE
|
||||
"""
|
||||
return self._selectObjects('Edges',selector)
|
||||
|
||||
def wires(self,selector=None):
|
||||
"""
|
||||
Select the wires of objects on the stack, optionally filtering the selection. If there are multiple objects
|
||||
on the stack, the wires of all objects are collected and a list of all the distinct wires is returned.
|
||||
|
||||
:param selector: A selector
|
||||
:type selector: None, a Selector object, or a string selector expression.
|
||||
:return: a CQ object whos stack contains all of the *distinct* wires of *all* objects on the current stack,
|
||||
filtered by the provided selector.
|
||||
|
||||
If there are no wires for any objects on the current stack, an empty CQ object is returned
|
||||
|
||||
The typical use is to select the wires of a single object on the stack. For example::
|
||||
|
||||
CQ(aCube).faces("+Z").wires().size()
|
||||
|
||||
returns 1, because a face typically only has one outer wire
|
||||
|
||||
See more about selectors HERE
|
||||
"""
|
||||
return self._selectObjects('Wires',selector)
|
||||
|
||||
def solids(self,selector=None):
|
||||
"""
|
||||
Select the solids of objects on the stack, optionally filtering the selection. If there are multiple objects
|
||||
on the stack, the solids of all objects are collected and a list of all the distinct solids is returned.
|
||||
|
||||
:param selector: A selector
|
||||
:type selector: None, a Selector object, or a string selector expression.
|
||||
:return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack,
|
||||
filtered by the provided selector.
|
||||
|
||||
If there are no solids for any objects on the current stack, an empty CQ object is returned
|
||||
|
||||
The typical use is to select the a single object on the stack. For example::
|
||||
|
||||
CQ(aCube).solids().size()
|
||||
|
||||
returns 1, because a cube consists of one solid.
|
||||
|
||||
It is possible for single CQ object ( or even a single CAD primitive ) to contain multiple solids.
|
||||
|
||||
See more about selectors HERE
|
||||
"""
|
||||
return self._selectObjects('Solids',selector)
|
||||
|
||||
def shells(self,selector=None):
|
||||
"""
|
||||
Select the shells of objects on the stack, optionally filtering the selection. If there are multiple objects
|
||||
on the stack, the shells of all objects are collected and a list of all the distinct shells is returned.
|
||||
|
||||
:param selector: A selector
|
||||
:type selector: None, a Selector object, or a string selector expression.
|
||||
:return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack,
|
||||
filtered by the provided selector.
|
||||
|
||||
If there are no shells for any objects on the current stack, an empty CQ object is returned
|
||||
|
||||
Most solids will have a single shell, which represents the outer surface. A shell will typically be
|
||||
composed of multiple faces.
|
||||
|
||||
See more about selectors HERE
|
||||
"""
|
||||
return self._selectObjects('Shells',selector)
|
||||
|
||||
def compounds(self,selector=None):
|
||||
"""
|
||||
Select compounds on the stack, optionally filtering the selection. If there are multiple objects
|
||||
on the stack, they are collected and a list of all the distinct compounds is returned.
|
||||
|
||||
:param selector: A selector
|
||||
:type selector: None, a Selector object, or a string selector expression.
|
||||
:return: a CQ object whos stack contains all of the *distinct* solids of *all* objects on the current stack,
|
||||
filtered by the provided selector.
|
||||
|
||||
A compound contains multiple CAD primitives that resulted from a single operation, such as a union, cut,
|
||||
split, or fillet. Compounds can contain multiple edges, wires, or solids.
|
||||
|
||||
See more about selectors HERE
|
||||
"""
|
||||
return self._selectObjects('Compounds',selector)
|
||||
|
||||
def toSvg(self,opts=None):
|
||||
"""
|
||||
Returns svg text that represents the first item on the stack.
|
||||
|
||||
for testing purposes.
|
||||
|
||||
:param options: svg formatting options
|
||||
:type options: dictionary, width and height
|
||||
:return: a string that contains SVG that represents this item.
|
||||
"""
|
||||
return SVGexporter.getSVG(self.val().wrapped,opts)
|
||||
|
||||
def exportSvg(self,fileName):
|
||||
"""
|
||||
Exports the first item on the stack as an SVG file
|
||||
|
||||
For testing purposes mainly.
|
||||
|
||||
:param fileName: the filename to export
|
||||
:type fileName: String, absolute path to the file
|
||||
|
||||
"""
|
||||
SVGexporter.exportSVG(self.val().wrapped,fileName)
|
||||
|
||||
def rotateAboutCenter(self,axisEndPoint,angleDegrees):
|
||||
"""
|
||||
Rotates all items on the stack by the specified angle, about the specified axis
|
||||
|
||||
The center of rotation is a vector starting at the center of the object on the stack,
|
||||
and ended at the specified point.
|
||||
|
||||
:param axisEndPoint: the second point of axis of rotation
|
||||
:type axisEndPoint: a three-tuple in global coordinates
|
||||
:param angleDegrees: the rotation angle, in degrees
|
||||
:type angleDegrees: float
|
||||
:returns: a CQ object, with all items rotated.
|
||||
|
||||
WARNING: This version returns the same cq object instead of a new one-- the
|
||||
old object is not accessible.
|
||||
|
||||
Future Enhancements:
|
||||
* A version of this method that returns a transformed copy, rather than modifying
|
||||
the originals
|
||||
* This method doesnt expose a very good interface, becaues the axis of rotation
|
||||
could be inconsistent between multiple objects. This is because the beginning
|
||||
of the axis is variable, while the end is fixed. This is fine when operating on
|
||||
one object, but is not cool for multiple.
|
||||
|
||||
"""
|
||||
|
||||
#center point is the first point in the vector
|
||||
endVec = Vector(axisEndPoint)
|
||||
|
||||
def _rot(obj):
|
||||
startPt = obj.Center()
|
||||
endPt = startPt + endVec
|
||||
obj.rotate(startPt,endPt,angleDegrees)
|
||||
|
||||
return self.each(_rot,False)
|
||||
|
||||
def translate(self,vec):
|
||||
"""
|
||||
Returns a copy of all of the items on the stack by the specified distance
|
||||
|
||||
:param tupleDistance: distance to move, in global coordinates
|
||||
:type tupleDistance: a 3-tuple of float
|
||||
:returns: a CQ object
|
||||
|
||||
WARNING: the underlying objects are modified, not copied.
|
||||
|
||||
Future Enhancements:
|
||||
A version of this method that returns a transformed copy instead
|
||||
of modifying the originals.
|
||||
"""
|
||||
return self.newObject([o.translate(vec) for o in self.objects])
|
||||
|
||||
|
||||
def shell(self,thickness):
|
||||
"""
|
||||
Remove the selected faces to create a shell of the specified thickness.
|
||||
|
||||
To shell, first create a solid, and *in the same chain* select the faces you wish to remove.
|
||||
|
||||
:param thickness: a positive float, representing the thickness of the desired shell. Negative values shell inwards,
|
||||
positive values shell outwards.
|
||||
:raises: ValueError if the current stack contains objects that are not faces of a solid further
|
||||
up in the chain.
|
||||
:returns: a CQ object with the resulting shelled solid selected.
|
||||
|
||||
This example will create a hollowed out unit cube, where the top most face is open,
|
||||
and all other walls are 0.2 units thick::
|
||||
|
||||
Workplane().box(1,1,1).faces("+Z").shell(0.2)
|
||||
|
||||
Shelling is one of the cases where you may need to use the add method to select several faces. For
|
||||
example, this example creates a 3-walled corner, by removing three faces of a cube::
|
||||
|
||||
s = Workplane().box(1,1,1)
|
||||
s1 = s.faces("+Z")
|
||||
s1.add(s.faces("+Y")).add(s.faces("+X"))
|
||||
self.saveModel(s1.shell(0.2))
|
||||
|
||||
This fairly yucky syntax for selecting multiple faces is planned for improvement
|
||||
|
||||
**Note**: When sharp edges are shelled inwards, they remain sharp corners, but **outward** shells are
|
||||
automatically filleted, because an outward offset from a corner generates a radius
|
||||
|
||||
|
||||
Future Enhancements:
|
||||
Better selectors to make it easier to select multiple faces
|
||||
|
||||
"""
|
||||
solidRef = self.findSolid()
|
||||
|
||||
for f in self.objects:
|
||||
if type(f) != Face:
|
||||
raise ValueError ("Shelling requires that faces be selected")
|
||||
|
||||
s = solidRef.shell(self.objects,thickness)
|
||||
solidRef.wrapped = s.wrapped
|
||||
return self.newObject([s])
|
||||
|
||||
|
||||
def fillet(self,radius):
|
||||
"""
|
||||
Fillets a solid on the selected edges.
|
||||
|
||||
The edges on the stack are filleted. The solid to which the edges belong must be in the parent chain
|
||||
of the selected edges.
|
||||
|
||||
:param radius: the radius of the fillet, must be > zero
|
||||
:type radius: positive float
|
||||
:raises: ValueError if at least one edge is not selected
|
||||
:raises: ValueError if the solid containing the edge is not in the chain
|
||||
:returns: cq object with the resulting solid selected.
|
||||
|
||||
This example will create a unit cube, with the top edges filleted::
|
||||
|
||||
s = Workplane().box(1,1,1).faces("+Z").edges().fillet(0.1)
|
||||
"""
|
||||
#TODO: we will need much better edge selectors for this to work
|
||||
#TODO: ensure that edges selected actually belong to the solid in the chain, otherwise, fe segfault
|
||||
|
||||
solid = self.findSolid()
|
||||
|
||||
edgeList = self.edges().vals()
|
||||
if len(edgeList) < 1:
|
||||
raise ValueError ("Fillets requires that edges be selected")
|
||||
|
||||
s = solid.fillet(radius,edgeList)
|
||||
solid.wrapped = s.wrapped
|
||||
return self.newObject([s])
|
||||
|
19
cadquery/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
#these items are the common implementation
|
||||
from .CQ import CQ
|
||||
from .workplane import Workplane
|
||||
from . import plugins
|
||||
from . import selectors
|
||||
|
||||
#these items point to the freecad implementation
|
||||
from .freecad_impl.geom import Plane,BoundBox,Vector
|
||||
from .freecad_impl.shapes import Shape,Vertex,Edge,Wire,Solid,Shell,Compound
|
||||
from .freecad_impl.exporters import SvgExporter, AmfExporter, JsonExporter
|
||||
|
||||
__all__ = [
|
||||
'CQ','Workplane','plugins','selectors','Plane','BoundBox',
|
||||
'Shape','Vertex','Edge','Wire','Solid','Shell','Compound',
|
||||
'SvgExporter','AmfExporter','JsonExporter',
|
||||
'plugins'
|
||||
]
|
||||
|
||||
__version__ = 0.9
|
0
cadquery/contrib/__init__.py
Normal file
5
cadquery/freecad_impl/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
import FreeCAD
|
||||
from FreeCAD import Part
|
||||
from FreeCAD import Base
|
||||
FreeCADVector = Base.Vector
|
||||
DEFAULT_TOLERANCE = 0.0001
|
297
cadquery/freecad_impl/exporters.py
Normal file
@ -0,0 +1,297 @@
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
class AMFWriter(object):
|
||||
def __init__(self,tessellation):
|
||||
|
||||
self.units = "mm"
|
||||
self.tessellation = tessellation
|
||||
|
||||
def writeAmf(self,outFileName):
|
||||
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(outFileName,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
|
||||
};
|
||||
|
||||
|
||||
class UNITS:
|
||||
MM = "mm"
|
||||
IN = "in"
|
||||
|
||||
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
|
||||
|
||||
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 <g> 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 = "<root>%s</root>" % 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):
|
||||
"""
|
||||
export a view of a part to svg
|
||||
"""
|
||||
svg = getSVG(shape)
|
||||
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 = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="%(width)s"
|
||||
height="%(height)s"
|
||||
|
||||
>
|
||||
<g transform="scale(%(unitScale)s, -%(unitScale)s) translate(%(xTranslate)s,%(yTranslate)s)" stroke-width="%(strokeWidth)s" fill="none">
|
||||
<!-- hidden lines -->
|
||||
<g stroke="rgb(160, 160, 160)" fill="none" stroke-dasharray="%(strokeWidth)s,%(strokeWidth)s" >
|
||||
%(hiddenContent)s
|
||||
</g>
|
||||
|
||||
<!-- solid lines -->
|
||||
<g stroke="rgb(0, 0, 0)" fill="none">
|
||||
%(visibleContent)s
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(20,%(textboxY)s)" stroke="rgb(0,0,255)">
|
||||
<line x1="30" y1="-30" x2="75" y2="-33" stroke-width="3" stroke="#000000" />
|
||||
<text x="80" y="-30" style="stroke:#000000">X </text>
|
||||
|
||||
<line x1="30" y1="-30" x2="30" y2="-75" stroke-width="3" stroke="#000000" />
|
||||
<text x="25" y="-85" style="stroke:#000000">Y </text>
|
||||
|
||||
<line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3" stroke="#000000" />
|
||||
<text x="65" y="-5" style="stroke:#000000">Z </text>
|
||||
<!--
|
||||
<line x1="0" y1="0" x2="%(unitScale)s" y2="0" stroke-width="3" />
|
||||
<text x="0" y="20" style="stroke:#000000">1 %(uom)s </text>
|
||||
-->
|
||||
</g>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
PATHTEMPLATE="\t\t\t<path d=\"%s\" />\n"
|
||||
|
541
cadquery/freecad_impl/geom.py
Normal file
@ -0,0 +1,541 @@
|
||||
"""
|
||||
A Parametric CAD System for the Web
|
||||
Copyright (c) 2010-2022 Parametric Products Intellectual Holdings LLC, All Rights Reserved.
|
||||
|
||||
Basic Geometry Constructs
|
||||
"""
|
||||
import math,sys
|
||||
|
||||
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
|
||||
|
||||
def sortWiresByBuildOrderOld(wireList,plane,result=[]):
|
||||
"""
|
||||
Tries to determine how wires should be combined into faces.
|
||||
Assume:
|
||||
The wires make up one or more faces, which could have 'holes'
|
||||
Outer wires are listed ahead of inner wires
|
||||
there are no wires inside wires inside wires ( IE, islands -- we can deal with that later on )
|
||||
none of the wires are construction wires
|
||||
Compute:
|
||||
one or more sets of wires, with the outer wire listed first, and inner ones
|
||||
Returns, list of lists.
|
||||
"""
|
||||
outerWire = wireList.pop(0)
|
||||
|
||||
remainingWires = list(wireList)
|
||||
childWires = []
|
||||
for w in wireList:
|
||||
if plane.isWireInside(outerWire,w):
|
||||
childWires.append(remainingWires.pop(0))
|
||||
else:
|
||||
#doesnt match, assume this wire is a new outer
|
||||
result.append([outerWire] + childWires)
|
||||
return sortWiresByBuildOrder(remainingWires,plane,result)
|
||||
|
||||
result.append([outerWire] + childWires)
|
||||
return result
|
||||
|
||||
class Vector(object):
|
||||
"""
|
||||
Create a 3-dimensional vector
|
||||
|
||||
:param *args: a 3-d vector, with x-y-z parts.
|
||||
|
||||
you can either provide:
|
||||
* a FreeCAD vector
|
||||
* a vector ( in which case it is copied )
|
||||
* a 3-tuple
|
||||
* three float values, x, y, and z
|
||||
|
||||
FreeCAD's vector implementation has a dumb
|
||||
implementation for multiply and add-- they modify the existing
|
||||
value and return a copy as well.
|
||||
|
||||
This vector is immutable-- all mutations return a copy!
|
||||
|
||||
"""
|
||||
def __init__(self,*args):
|
||||
|
||||
if len(args) == 3:
|
||||
fV = FreeCADVector(args[0],args[1],args[2])
|
||||
elif len(args) == 1:
|
||||
if type(args[0]) is tuple:
|
||||
fV = FreeCADVector(args[0][0],args[0][1],args[0][2])
|
||||
elif type(args[0] is FreeCADVector):
|
||||
fV = args[0]
|
||||
elif type(args[0] is Vector):
|
||||
fV = args[0].wrapped
|
||||
else:
|
||||
fV = args[0]
|
||||
else:
|
||||
raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple")
|
||||
|
||||
self.wrapped = fV
|
||||
self.Length = fV.Length
|
||||
self.x = fV.x
|
||||
self.y = fV.y
|
||||
self.z = fV.z
|
||||
|
||||
def toTuple(self):
|
||||
return (self.x,self.y,self.z)
|
||||
|
||||
#TODO: is it possible to create a dynamic proxy without all this code?
|
||||
def cross(self,v):
|
||||
return Vector( self.wrapped.cross(v.wrapped))
|
||||
|
||||
def dot(self,v):
|
||||
return self.wrapped.dot(v.wrapped)
|
||||
|
||||
def sub(self,v):
|
||||
return self.wrapped.sub(v.wrapped)
|
||||
|
||||
def add(self,v):
|
||||
return Vector( self.wrapped.add(v.wrapped))
|
||||
|
||||
def multiply(self,scale):
|
||||
"""
|
||||
Return self multiplied by the provided scalar
|
||||
|
||||
Note: FreeCAD has a bug here, where the
|
||||
base is also modified
|
||||
"""
|
||||
tmp = FreeCADVector(self.wrapped)
|
||||
return Vector( tmp.multiply(scale))
|
||||
|
||||
def normalize(self):
|
||||
"""
|
||||
Return normalized version this vector.
|
||||
|
||||
Note: FreeCAD has a bug here, where the
|
||||
base is also modified
|
||||
"""
|
||||
tmp = FreeCADVector(self.wrapped)
|
||||
tmp.normalize()
|
||||
return Vector( tmp )
|
||||
|
||||
def Center(self):
|
||||
"""
|
||||
The center of myself is myself.
|
||||
Provided so that vectors, vertexes, and other shapes all support a common interface,
|
||||
when Center() is requested for all objects on the stack
|
||||
"""
|
||||
return self
|
||||
|
||||
def getAngle(self,v):
|
||||
return self.wrapped.getAngle(v.wrapped)
|
||||
|
||||
def distanceToLine(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def projectToLine(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def distanceToPlane(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def projectToPlane(self):
|
||||
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!")
|
||||
|
||||
def __hash__(self):
|
||||
return self.wrapped.__hash__()
|
||||
|
||||
def __add__(self,v):
|
||||
return self.add(v)
|
||||
|
||||
def __len__(self):
|
||||
return self.Length
|
||||
|
||||
def __repr__(self):
|
||||
return self.wrapped.__repr__()
|
||||
|
||||
def __str__(self):
|
||||
return self.wrapped.__str__()
|
||||
|
||||
def __len__(self,other):
|
||||
return self.wrapped.__len__(other)
|
||||
|
||||
def __lt__(self,other):
|
||||
return self.wrapped.__lt__(other)
|
||||
|
||||
def __gt__(self,other):
|
||||
return self.wrapped.__gt__(other)
|
||||
|
||||
def __ne__(self,other):
|
||||
return self.wrapped.__ne__(other)
|
||||
|
||||
def __le__(self,other):
|
||||
return self.wrapped.__le__(other)
|
||||
|
||||
def __ge__(self,other):
|
||||
return self.wrapped.__ge__(other)
|
||||
|
||||
def __eq__(self,other):
|
||||
return self.wrapped.__eq__(other)
|
||||
|
||||
class Plane:
|
||||
"""
|
||||
A 2d coordinate system in space, with the x-y axes on the a plane, and a particular point as the origin.
|
||||
|
||||
A plane allows the use of 2-d coordinates, which are later converted to global, 3d coordinates when
|
||||
the operations are complete.
|
||||
|
||||
Frequently, it is not necessary to create work planes, as they can be created automatically from faces.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def named(cls,stdName,origin=(0,0,0)):
|
||||
"""
|
||||
Create a predefined Plane based on the conventional names.
|
||||
|
||||
:param stdName: one of (XY|YZ|XZ|front|back|left|right|top|bottom
|
||||
:type stdName: string
|
||||
:param origin: the desired origin, specified in global coordinates
|
||||
:type origin: 3-tuple of the origin of the new plane, in global coorindates.
|
||||
|
||||
Available named planes are as follows. Direction references refer to the global
|
||||
directions
|
||||
|
||||
=========== ======= ======= ======
|
||||
Name xDir yDir zDir
|
||||
=========== ======= ======= ======
|
||||
XY +x +y +z
|
||||
YZ +y +z +x
|
||||
XZ +x +z -y
|
||||
front +x +y +z
|
||||
back -x +y -z
|
||||
left +z +y -x
|
||||
right -z +y +x
|
||||
top +x -z +y
|
||||
bottom +x +z -y
|
||||
=========== ======= ======= ======
|
||||
"""
|
||||
|
||||
namedPlanes = {
|
||||
#origin, xDir, normal
|
||||
'XY' : Plane(Vector(origin),Vector((1,0,0)),Vector((0,0,1))),
|
||||
'YZ' : Plane(Vector(origin),Vector((0,1,0)),Vector((1,0,0))),
|
||||
'XZ' : Plane(Vector(origin),Vector((1,0,0)),Vector((0,-1,0))),
|
||||
'front': Plane(Vector(origin),Vector((1,0,0)),Vector((0,0,1))),
|
||||
'back': Plane(Vector(origin),Vector((-1,0,0)),Vector((0,0,-1))),
|
||||
'left': Plane(Vector(origin),Vector((0,0,1)),Vector((-1,0,0))),
|
||||
'right': Plane(Vector(origin),Vector((0,0,-1)),Vector((1,0,0))),
|
||||
'top': Plane(Vector(origin),Vector((1,0,0)),Vector((0,1,0))),
|
||||
'bottom': Plane(Vector(origin),Vector((1,0,0)),Vector((0,-1,0)))
|
||||
}
|
||||
|
||||
if namedPlanes.has_key(stdName):
|
||||
return namedPlanes[stdName]
|
||||
else:
|
||||
raise ValueError("Supported names are %s " % str(namedPlanes.keys()) )
|
||||
|
||||
@classmethod
|
||||
def XY(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('XY',origin)
|
||||
|
||||
@classmethod
|
||||
def YZ(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('YZ',origin)
|
||||
|
||||
@classmethod
|
||||
def XZ(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('XZ',origin)
|
||||
|
||||
@classmethod
|
||||
def front(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('front',origin)
|
||||
|
||||
@classmethod
|
||||
def back(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('back',origin)
|
||||
|
||||
@classmethod
|
||||
def left(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('left',origin)
|
||||
|
||||
@classmethod
|
||||
def right(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('right',origin)
|
||||
|
||||
@classmethod
|
||||
def top(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('top',origin)
|
||||
|
||||
@classmethod
|
||||
def bottom(cls,origin=(0,0,0),xDir=Vector(1,0,0)):
|
||||
return Plane.named('bottom',origin)
|
||||
|
||||
def __init__(self, origin, xDir, normal ):
|
||||
"""
|
||||
Create a Plane with an arbitrary orientation
|
||||
|
||||
TODO: project x and y vectors so they work even if not orthogonal
|
||||
:param origin: the origin
|
||||
:type origin: a three-tuple of the origin, in global coordinates
|
||||
:param xDir: a vector representing the xDirection.
|
||||
:type xDir: a three-tuple representing a vector, or a FreeCAD Vector
|
||||
:param normal: the normal direction for the new plane
|
||||
:type normal: a FreeCAD Vector
|
||||
:raises: ValueError if the specified xDir is not orthogonal to the provided normal.
|
||||
:return: a plane in the global space, with the xDirection of the plane in the specified direction.
|
||||
|
||||
"""
|
||||
self.xDir = xDir.normalize()
|
||||
self.yDir = normal.cross(self.xDir).normalize()
|
||||
self.zDir = normal.normalize()
|
||||
|
||||
#stupid freeCAD!!!!! multiply has a bug that changes the original also!
|
||||
self.invZDir = self.zDir.multiply(-1.0)
|
||||
|
||||
self.setOrigin3d(origin)
|
||||
|
||||
def setOrigin3d(self,originVector):
|
||||
"""
|
||||
Move the origin of the plane, leaving its orientation and xDirection unchanged.
|
||||
:param originVector: the new center of the plane, *global* coordinates
|
||||
:type originVector: a FreeCAD Vector.
|
||||
:return: void
|
||||
|
||||
"""
|
||||
self.origin = originVector
|
||||
self._calcTransforms()
|
||||
|
||||
def setOrigin2d(self,x,y):
|
||||
"""
|
||||
Set a new origin based of the plane. The plane's orientation and xDrection are unaffected.
|
||||
|
||||
:param float x: offset in the x direction
|
||||
:param float y: offset in the y direction
|
||||
:return: void
|
||||
|
||||
the new coordinates are specified in terms of the current 2-d system. As an example::
|
||||
p = Plane.XY()
|
||||
p.setOrigin2d(2,2)
|
||||
p.setOrigin2d(2,2)
|
||||
|
||||
results in a plane with its origin at (x,y)=(4,4) in global coordinates. The both operations were relative to
|
||||
local coordinates of the plane.
|
||||
|
||||
"""
|
||||
self.setOrigin3d(self.toWorldCoords((x,y)))
|
||||
|
||||
|
||||
def isWireInside(self,baseWire,testWire):
|
||||
"""
|
||||
Determine if testWire is inside baseWire, after both wires are projected into the current plane
|
||||
|
||||
:param baseWire: a reference wire
|
||||
:type baseWire: a FreeCAD wire
|
||||
:param testWire: another wire
|
||||
:type testWire: a FreeCAD wire
|
||||
:return: True if testWire is inside baseWire, otherwise False
|
||||
|
||||
If either wire does not lie in the current plane, it is projected into the plane first.
|
||||
|
||||
*WARNING*: This method is not 100% reliable. It uses bounding box tests, but needs
|
||||
more work to check for cases when curves are complex.
|
||||
|
||||
Future Enhancements:
|
||||
* Discretizing points along each curve to provide a more reliable test
|
||||
|
||||
"""
|
||||
#TODO: also use a set of points along the wire to test as well.
|
||||
#TODO: would it be more efficient to create objects in the local coordinate system, and then transform to global
|
||||
#coordinates upon extrusion?
|
||||
|
||||
tBaseWire = baseWire.transformGeometry(self.fG)
|
||||
tTestWire = testWire.transformGeometry(self.fG)
|
||||
|
||||
#these bounding boxes will have z=0, since we transformed them into the space of the plane
|
||||
bb = tBaseWire.BoundingBox()
|
||||
tb = tTestWire.BoundingBox()
|
||||
|
||||
#findOutsideBox actually inspects both ways, here we only want to
|
||||
#know if one is inside the other
|
||||
x = BoundBox.findOutsideBox2D(bb,tb)
|
||||
return x == bb
|
||||
|
||||
def toLocalCoords(self,obj):
|
||||
"""
|
||||
Project the provided coordinates onto this plane.
|
||||
|
||||
:param obj: an object or vector to convert
|
||||
:type vector: a vector or shape
|
||||
:return: an object of the same type as the input, but converted to local coordinates
|
||||
|
||||
|
||||
Most of the time, the z-coordinate returned will be zero, because most operations
|
||||
based on a plane are all 2-d. Occasionally, though, 3-d points outside of the current plane are transformed.
|
||||
One such example is :py:meth:`Workplane.box`, where 3-d corners of a box are transformed to orient the box in space
|
||||
correctly.
|
||||
|
||||
"""
|
||||
if isinstance(obj,Vector):
|
||||
return Vector(self.fG.multiply(obj.wrapped))
|
||||
elif isinstance(obj,Shape):
|
||||
return obj.transformShape(self.rG)
|
||||
else:
|
||||
raise ValueError("Dont know how to convert type %s to local coordinates" % str(type(obj)))
|
||||
|
||||
def toWorldCoords(self, tuplePoint):
|
||||
"""
|
||||
Convert a point in local coordinates to global coordinates.
|
||||
|
||||
:param tuplePoint: point in local coordinates to convert
|
||||
:type tuplePoint: a 2 or three tuple of float. the third value is taken to be zero if not supplied
|
||||
:return: a 3-tuple in global coordinates
|
||||
|
||||
|
||||
"""
|
||||
if len(tuplePoint) == 2:
|
||||
v = Vector(tuplePoint[0], tuplePoint[1], 0)
|
||||
else:
|
||||
v = Vector(tuplePoint[0],tuplePoint[1],tuplePoint[2])
|
||||
return Vector(self.rG.multiply(v.wrapped))
|
||||
|
||||
def rotated(self,rotate=Vector(0,0,0)):
|
||||
"""
|
||||
returns a copy of this plane, rotated about the specified axes, as measured from horizontal
|
||||
|
||||
Since the z axis is always normal the plane, rotating around Z will always produce a plane
|
||||
that is parallel to this one
|
||||
|
||||
the origin of the workplane is unaffected by the rotation.
|
||||
|
||||
rotations are done in order x,y,z. if you need a different order, manually chain together multiple .rotate()
|
||||
commands
|
||||
|
||||
:param roate: Vector [xDegrees,yDegrees,zDegrees]
|
||||
:return: a copy of this plane rotated as requested
|
||||
"""
|
||||
|
||||
#convert to radians
|
||||
rotate = rotate.multiply(math.pi / 180.0 )
|
||||
|
||||
#compute rotation matrix
|
||||
m = Base.Matrix()
|
||||
m.rotateX(rotate.x)
|
||||
m.rotateY(rotate.y)
|
||||
m.rotateZ(rotate.z)
|
||||
|
||||
#compute the new plane
|
||||
newXdir = Vector(m.multiply(self.xDir.wrapped))
|
||||
newZdir = Vector(m.multiply(self.zDir.wrapped))
|
||||
|
||||
newP= Plane(self.origin,newXdir,newZdir)
|
||||
return newP
|
||||
|
||||
def _calcTransforms(self):
|
||||
"""
|
||||
Computes transformation martrices to convert betwene local and global coordinates
|
||||
"""
|
||||
#r is the forward transformation matrix from world to local coordinates
|
||||
#ok i will be really honest-- i cannot understand exactly why this works
|
||||
#something bout the order of the transaltion and the rotation.
|
||||
# the double-inverting is strange, and i dont understand it.
|
||||
r = Base.Matrix()
|
||||
|
||||
#forward transform must rotate and adjust for origin
|
||||
(r.A11, r.A12, r.A13 ) = (self.xDir.x, self.xDir.y, self.xDir.z )
|
||||
(r.A21, r.A22, r.A23 ) = (self.yDir.x, self.yDir.y, self.yDir.z )
|
||||
(r.A31, r.A32, r.A33 ) = (self.zDir.x, self.zDir.y, self.zDir.z )
|
||||
|
||||
invR = r.inverse()
|
||||
(invR.A14,invR.A24,invR.A34) = (self.origin.x,self.origin.y,self.origin.z)
|
||||
|
||||
( self.rG,self.fG ) = ( invR,invR.inverse() )
|
||||
|
||||
|
||||
class BoundBox(object):
|
||||
"A BoundingBox for an object or set of objects. Wraps the FreeCAD one"
|
||||
def __init__(self,bb):
|
||||
self.wrapped = bb
|
||||
self.xmin = bb.XMin
|
||||
self.xmax = bb.XMax
|
||||
self.xlen = bb.XLength
|
||||
self.ymin = bb.YMin
|
||||
self.ymax = bb.YMax
|
||||
self.ylen = bb.YLength
|
||||
self.zmin = bb.ZMin
|
||||
self.zmax = bb.ZMax
|
||||
self.zlen = bb.ZLength
|
||||
self.center = Vector(bb.Center)
|
||||
self.DiagonalLength = bb.DiagonalLength
|
||||
|
||||
def add(self,obj):
|
||||
"""
|
||||
returns a modified (expanded) bounding box
|
||||
|
||||
obj can be one of several things:
|
||||
1. a 3-tuple corresponding to x,y, and z amounts to add
|
||||
2. a vector, containing the x,y,z values to add
|
||||
3. another bounding box, where a new box will be created that encloses both
|
||||
|
||||
this bounding box is not changed
|
||||
"""
|
||||
tmp = FreeCAD.Base.BoundBox(self.wrapped)
|
||||
if type(obj) is tuple:
|
||||
tmp.add(obj[0],obj[1],obj[2])
|
||||
elif type(obj) is Vector:
|
||||
tmp.add(obj.fV)
|
||||
elif type(obj) is BoundBox:
|
||||
tmp.add(obj.wrapped)
|
||||
|
||||
return BoundBox(tmp)
|
||||
|
||||
@classmethod
|
||||
def findOutsideBox2D(cls,b1, b2):
|
||||
"""
|
||||
compares bounding boxes. returns none if neither is inside the other. returns
|
||||
the outer one if either is outside the other
|
||||
|
||||
BoundBox.isInside works in 3d, but this is a 2d bounding box, so it doesnt work correctly
|
||||
plus, there was all kinds of rounding error in the built-in implementation i do not understand.
|
||||
Here we assume that the b
|
||||
"""
|
||||
bb1 = b1.wrapped
|
||||
bb2 = b2.wrapped
|
||||
if bb1.XMin < bb2.XMin and\
|
||||
bb1.XMax > bb2.XMax and\
|
||||
bb1.YMin < bb2.YMin and\
|
||||
bb1.YMax > bb2.YMax:
|
||||
return b1
|
||||
|
||||
if bb2.XMin < bb1.XMin and\
|
||||
bb2.XMax > bb1.XMax and\
|
||||
bb2.YMin < bb1.YMin and\
|
||||
bb2.YMax > bb1.YMax:
|
||||
return b2
|
||||
|
||||
return None
|
||||
|
||||
def isInside(self,anotherBox):
|
||||
"""
|
||||
is the provided bounding box inside this one?
|
||||
"""
|
||||
return self.wrapped.isInside(anotherBox.wrapped)
|
789
cadquery/freecad_impl/shapes.py
Normal file
@ -0,0 +1,789 @@
|
||||
"""
|
||||
Wrapper Classes for FreeCAD
|
||||
These classes provide a stable interface for 3d objects,
|
||||
independent of the FreeCAD interface.
|
||||
|
||||
Future work might include use of pythonOCC, OCC, or even
|
||||
another CAD kernel directly, so this interface layer is quite important.
|
||||
|
||||
Funny, in java this is one of those few areas where i'd actually spend the time
|
||||
to make an interface and an implementation, but for new these are just rolled together
|
||||
|
||||
This interface layer provides three distinct values:
|
||||
|
||||
1. It allows us to avoid changing key api points if we change underlying implementations.
|
||||
It would be a disaster if script and plugin authors had to change models because we
|
||||
changed implmentations
|
||||
|
||||
2. Allow better documentation. One of the reasons FreeCAD is no more popular is because
|
||||
its docs are terrible. This allows us to provie good documentation via docstrings
|
||||
for each wrapper
|
||||
|
||||
3. Work around bugs. there are a quite a feb bugs in free this layer allows fixing them
|
||||
|
||||
4. allows for enhanced functionality. Many objects are missing features we need. For example
|
||||
we need a 'forConstruciton' flag on the Wire object. this allows adding those kinds of things
|
||||
|
||||
5. allow changing interfaces when we'd like. there are few cases where the freecad api is not
|
||||
very userfriendly: we like to change those when necesary. As an example, in the freecad api,
|
||||
all factory methods are on the 'Part' object, but it is very useful to know what kind of
|
||||
object each one returns, so these are better grouped by the type of object they return.
|
||||
(who would know that Part.makeCircle() returns an Edge, but Part.makePolygon() returns a Wire ?
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ExportFormats:
|
||||
STL = "STL"
|
||||
BREP = "BREP"
|
||||
STEP = "STEP"
|
||||
AMF = "AMF"
|
||||
IGES = "IGES"
|
||||
|
||||
|
||||
class Shape(object):
|
||||
"""
|
||||
Represents a shape in the system.
|
||||
Wrappers the FreeCAD api
|
||||
"""
|
||||
|
||||
def __init__(self,obj):
|
||||
self.wrapped = obj
|
||||
self.forConstruction = False
|
||||
|
||||
@classmethod
|
||||
def cast(cls,obj,forConstruction = False):
|
||||
"Returns the right type of wrapper, given a FreeCAD object"
|
||||
s = obj.ShapeType
|
||||
if type(obj) == FreeCADVector:
|
||||
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
|
||||
|
||||
def exportStl(self,fileName):
|
||||
self.wrapped.exportStl(fileName)
|
||||
|
||||
def exportStep(self,fileName):
|
||||
self.wrapped.exportStep(fileName)
|
||||
|
||||
def exportShape(self,fileName, fileFormat):
|
||||
if fileFormat == ExportFormats.STL:
|
||||
self.wrapped.exportStl(fileName)
|
||||
elif fileFormat == ExportFormats.BREP:
|
||||
self.wrapped.exportBrep(fileName)
|
||||
elif fileFormat == ExportFormats.STEP:
|
||||
self.wrapped.exportStep(fileName)
|
||||
elif fileFormat == ExportFormats.AMF:
|
||||
#not built into FreeCAD
|
||||
#TODO: user selected tolerance
|
||||
tess = self.wrapped.tessellate(0.1)
|
||||
aw = amfUtils.AMFWriter(tess)
|
||||
aw.writeAmf(fileName)
|
||||
elif fileFormat == ExportFormats.IGES:
|
||||
self.wrapped.exportIges(fileName)
|
||||
else:
|
||||
raise ValueError("Unknown export format: %s" % format)
|
||||
|
||||
def geomType(self):
|
||||
"""
|
||||
Gets the underlying geometry type
|
||||
:return: a string according to the geometry type.
|
||||
|
||||
Implementations can return any values desired, but the
|
||||
values the user uses in type filters should correspond to these.
|
||||
|
||||
As an example, if a user does::
|
||||
|
||||
CQ(object).faces("%mytype")
|
||||
|
||||
The expectation is that the geomType attribute will return 'mytype'
|
||||
|
||||
The return values depend on the type of the shape:
|
||||
|
||||
Vertex: always 'Vertex'
|
||||
Edge: LINE, ARC, CIRCLE, SPLINE
|
||||
Face: PLANE, SPHERE, CONE
|
||||
Solid: 'Solid'
|
||||
Shell: 'Shell'
|
||||
Compound: 'Compound'
|
||||
Wire: 'Wire'
|
||||
"""
|
||||
return self.wrapped.ShapeType
|
||||
|
||||
def isType(self,obj,strType):
|
||||
"""
|
||||
Returns True if the shape is the specified type, false otherwise
|
||||
|
||||
contrast with ShapeType, which will raise an exception
|
||||
if the provide object is not a shape at all
|
||||
"""
|
||||
if hasattr(obj,'ShapeType'):
|
||||
return obj.ShapeType == strType
|
||||
else:
|
||||
return False
|
||||
|
||||
def hashCode(self):
|
||||
return self.wrapped.hashCode()
|
||||
|
||||
def isNull(self):
|
||||
return self.wrapped.isNull()
|
||||
|
||||
def isSame(self,other):
|
||||
return self.wrapped.isSame(other.wrapped)
|
||||
|
||||
def isEqual(self,other):
|
||||
return self.wrapped.isEqual(other.wrapped)
|
||||
|
||||
def isValid(self):
|
||||
return self.wrapped.isValid()
|
||||
|
||||
def BoundingBox(self):
|
||||
return BoundBox(self.wrapped.BoundBox)
|
||||
|
||||
def Center(self):
|
||||
try:
|
||||
return Vector(self.wrapped.CenterOfMass)
|
||||
except:
|
||||
pass
|
||||
def Closed(self):
|
||||
return self.wrapped.Closed
|
||||
|
||||
def ShapeType(self):
|
||||
return self.wrapped.ShapeType
|
||||
|
||||
def Vertices(self):
|
||||
return [Vertex(i) for i in self.wrapped.Vertexes]
|
||||
|
||||
def Edges(self):
|
||||
return [Edge(i) for i in self.wrapped.Edges]
|
||||
|
||||
def Compounds(self):
|
||||
return [Compound(i) for i in self.wrapped.Compounds]
|
||||
|
||||
def Wires(self):
|
||||
return [Wire(i) for i in self.wrapped.Wires]
|
||||
|
||||
def Faces(self):
|
||||
return [Face(i) for i in self.wrapped.Faces]
|
||||
|
||||
def Shells(self):
|
||||
return [Shell(i) for i in self.wrapped.Shells]
|
||||
|
||||
def Solids(self):
|
||||
return [Solid(i) for i in self.wrapped.Solids]
|
||||
|
||||
def Area(self):
|
||||
return self.wrapped.Area
|
||||
|
||||
def Length(self):
|
||||
return self.wrapped.Length
|
||||
|
||||
def rotate(self,startVector,endVector,angleDegrees):
|
||||
"""
|
||||
Rotates a shape around an axis
|
||||
:param startVector: start point of rotation axis either a 3-tuple or a Vector
|
||||
:param endVector: end point of rotation axis, either a 3-tuple or a Vector
|
||||
:param angleDegrees: angle to rotate, in degrees
|
||||
:return: a copy of the shape, rotated
|
||||
"""
|
||||
if type(startVector) == tuple:
|
||||
startVector = Vector(startVector)
|
||||
|
||||
if type(endVector) == tuple:
|
||||
endVector = Vector(endVector)
|
||||
|
||||
tmp = self.wrapped.copy()
|
||||
tmp.rotate(startVector.wrapped,endVector.wrapped,angleDegrees)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def translate(self,vector):
|
||||
|
||||
if type(vector) == tuple:
|
||||
vector = Vector(vector)
|
||||
tmp = self.wrapped.copy()
|
||||
tmp.translate(vector.wrapped)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def scale(self,factor):
|
||||
tmp = self.wrapped.copy()
|
||||
tmp.scale(factor)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def copy(self):
|
||||
return Shape.cast(self.wrapped.copy())
|
||||
|
||||
def transformShape(self,tMatrix):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
returns a copy of the object, but with geometry transformed insetad of just
|
||||
rotated.
|
||||
|
||||
WARNING: transformGeometry will sometimes convert lines and circles to splines,
|
||||
but it also has the ability to handle skew and stretching transformations.
|
||||
|
||||
If your transformation is only translation and rotation, it is safer to use transformShape,
|
||||
which doesnt change the underlying type of the geometry, but cannot handle skew transformations
|
||||
"""
|
||||
tmp = self.wrapped.copy()
|
||||
tmp = tmp.transformGeometry(tMatrix)
|
||||
return Shape.cast(tmp)
|
||||
|
||||
def __hash__(self):
|
||||
return self.wrapped.hashCode()
|
||||
|
||||
class Vertex(Shape):
|
||||
def __init__(self,obj,forConstruction=False):
|
||||
"""
|
||||
Create a vertex from a FreeCAD Vertex
|
||||
"""
|
||||
self.wrapped = obj
|
||||
self.forConstruction = forConstruction
|
||||
self.X = obj.X
|
||||
self.Y = obj.Y
|
||||
self.Z = obj.Z
|
||||
|
||||
def toTuple(self):
|
||||
return (self.X,self.Y,self.Z)
|
||||
|
||||
def Center(self):
|
||||
"""
|
||||
The center of a vertex is itself!
|
||||
"""
|
||||
return Vector(self.wrapped.Point)
|
||||
|
||||
class Edge(Shape):
|
||||
def __init__(self,obj):
|
||||
"""
|
||||
An Edge
|
||||
"""
|
||||
self.wrapped = obj
|
||||
#self.startPoint = None
|
||||
#self.endPoint = None
|
||||
|
||||
self.edgetypes= {
|
||||
Part.Line : 'LINE',
|
||||
Part.ArcOfCircle : 'ARC',
|
||||
Part.Circle : 'CIRCLE'
|
||||
}
|
||||
|
||||
def geomType(self):
|
||||
t = type(self.wrapped.Curve)
|
||||
if self.edgetypes.has_key(t):
|
||||
return self.edgetypes[t]
|
||||
else:
|
||||
return "Unknown Edge Curve Type: %s" % str(t)
|
||||
|
||||
def startPoint(self):
|
||||
"""
|
||||
|
||||
:return: a vector representing the start poing of this edge
|
||||
|
||||
Note, circles may have the start and end points the same
|
||||
"""
|
||||
#work around freecad bug where valueAt is unreliable
|
||||
curve = self.wrapped.Curve
|
||||
return Vector( curve.value(self.wrapped.ParameterRange[0]))
|
||||
|
||||
def endPoint(self):
|
||||
"""
|
||||
|
||||
:return: a vector representing the end point of this edge.
|
||||
|
||||
Note, circles may have the start and end points the same
|
||||
|
||||
"""
|
||||
#warning: easier syntax in freecad of <Edge>.valueAt(<Edge>.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(Part.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 = Part.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 = Part.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(Part.makeLine(v1.toTuple(),v2.toTuple() ))
|
||||
|
||||
|
||||
class Wire(Shape):
|
||||
def __init__(self,obj):
|
||||
"""
|
||||
A Wire
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
@classmethod
|
||||
def combine(cls,listOfWires):
|
||||
"""
|
||||
Attempt to combine a list of wires into a new wire.
|
||||
the wires are returned in a list.
|
||||
:param cls:
|
||||
:param listOfWires:
|
||||
:return:
|
||||
"""
|
||||
return Shape.cast(Part.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( Part.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(Part.Wire([Part.makeCircle(radius,center.wrapped,normal.wrapped)]))
|
||||
return w
|
||||
|
||||
@classmethod
|
||||
def makePolygon(cls,listOfVertices,forConstruction=False):
|
||||
#convert list of tuples into Vectors.
|
||||
w = Wire(Part.makePolygon([i.wrapped for i in listOfVertices]))
|
||||
w.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(Part.makeHelix(pitch,height,radius,angle))
|
||||
|
||||
|
||||
class Face(Shape):
|
||||
def __init__(self,obj):
|
||||
"""
|
||||
A Face
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
self.facetypes = {
|
||||
#TODO: bezier,bspline etc
|
||||
Part.Plane : 'PLANE',
|
||||
Part.Sphere : 'SPHERE',
|
||||
Part.Cone : 'CONE'
|
||||
}
|
||||
|
||||
def geomType(self):
|
||||
t = type(self.wrapped.Surface)
|
||||
if self.facetypes.has_key(t):
|
||||
return self.facetypes[t]
|
||||
else:
|
||||
return "Unknown Face Surface Type: %s" % str(t)
|
||||
|
||||
def normalAt(self,locationVector=None):
|
||||
"""
|
||||
Computes the normal vector at the desired location on the face.
|
||||
|
||||
:returns: a vector representing the direction
|
||||
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
|
||||
:type locationVector: a vector that lies on the surface.
|
||||
"""
|
||||
if locationVector == None:
|
||||
locationVector = self.Center()
|
||||
(u,v) = self.wrapped.Surface.parameter(locationVector.wrapped)
|
||||
|
||||
return Vector(self.wrapped.normalAt(u,v).normalize() )
|
||||
|
||||
@classmethod
|
||||
def makePlane(cls,length,width,basePnt=None,dir=None):
|
||||
return Face(Part.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(Part.makeRuledSurface(edgeOrWire1.obj,edgeOrWire2.obj,dist))
|
||||
|
||||
def cut(self,faceToCut):
|
||||
"Remove a face from another one"
|
||||
return Shape.cast(self.obj.cut(faceToCut.obj))
|
||||
|
||||
def fuse(self,faceToJoin):
|
||||
return Shape.cast(self.obj.fuse(faceToJoin.obj))
|
||||
|
||||
def intersect(self,faceToIntersect):
|
||||
"""
|
||||
computes the intersection between the face and the supplied one.
|
||||
The result could be a face or a compound of faces
|
||||
"""
|
||||
return Shape.cast(self.obj.common(faceToIntersect.obj))
|
||||
|
||||
|
||||
class Shell(Shape):
|
||||
def __init__(self,wrapped):
|
||||
"""
|
||||
A Shell
|
||||
"""
|
||||
self.wrapped = wrapped
|
||||
|
||||
@classmethod
|
||||
def makeShell(cls,listOfFaces):
|
||||
return Shell(Part.makeShell([i.obj for i in listOfFaces]))
|
||||
|
||||
|
||||
class Solid(Shape):
|
||||
def __init__(self,obj):
|
||||
"""
|
||||
A Solid
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
@classmethod
|
||||
def isSolid(cls,obj):
|
||||
"""
|
||||
Returns true if the object is a FreeCAD solid, false otherwise
|
||||
"""
|
||||
if hasattr(obj, 'ShapeType'):
|
||||
if obj.ShapeType == 'Solid' or\
|
||||
(obj.ShapeType == 'Compound' and len(obj.Solids) > 0):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def makeBox(cls,length,width,height,pnt=Vector(0,0,0),dir=Vector(0,0,1)):
|
||||
"""
|
||||
makeBox(length,width,height,[pnt,dir]) -- Make a box located\nin pnt with the d
|
||||
imensions (length,width,height)\nBy default pnt=Vector(0,0,0) and dir=Vector(0,0,1)'
|
||||
"""
|
||||
return Shape.cast(Part.makeBox(length,width,height,pnt.wrapped,dir.wrapped))
|
||||
|
||||
@classmethod
|
||||
def makeCone(cls,radius1,radius2,height,pnt=Vector(0,0,0),dir=Vector(0,0,1),angleDegrees=360):
|
||||
"""
|
||||
'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(Part.makeCone(radius1,radius2,height,pnt.wrapped,dir.wrapped,angleDegrees))
|
||||
|
||||
@classmethod
|
||||
def makeCylinder(cls,radius,height,pnt=Vector(0,0,0),dir=Vector(0,0,1),angleDegrees=360):
|
||||
"""
|
||||
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(Part.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(Part.makeTorus(radius1,radius2,pnt,dir,angleDegrees1,angleDegrees2))
|
||||
|
||||
@classmethod
|
||||
def sweep(cls,profileWire,pathWire):
|
||||
"""
|
||||
make a solid by sweeping the profileWire along the specified path
|
||||
:param cls:
|
||||
:param profileWire:
|
||||
:param pathWire:
|
||||
:return:
|
||||
"""
|
||||
#needs to use freecad wire.makePipe or makePipeShell
|
||||
#needs to allow free-space wires ( those not made from a workplane )
|
||||
|
||||
@classmethod
|
||||
def makeLoft(cls,listOfWire):
|
||||
"""
|
||||
makes a loft from a list of wires
|
||||
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
|
||||
wants to make an infinitely thin shell for a real part.
|
||||
"""
|
||||
#the True flag requests building a solid instead of a shell.
|
||||
|
||||
return Shape.cast(Part.makeLoft([i.wrapped for i in listOfWire],True))
|
||||
|
||||
@classmethod
|
||||
def makeWedge(cls,xmin,ymin,zmin,z2min,x2min,xmax,ymax,zmax,z2max,x2max,pnt=None,dir=None):
|
||||
"""
|
||||
'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(Part.makeWedge(xmin,ymin,zmin,z2min,x2min,xmax,ymax,zmax,z2max,x2max,pnt,dir))
|
||||
|
||||
@classmethod
|
||||
def makeSphere(cls,radius,pnt=None,angleDegrees1=None,angleDegrees2=None,angleDegrees3=None):
|
||||
"""
|
||||
'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(Part.makeSphere(radius,pnt,angleDegrees1,angleDegrees2,angleDegrees3))
|
||||
|
||||
@classmethod
|
||||
def extrudeLinearWithRotation(cls,outerWire,innerWires,vecCenter, vecNormal,angleDegrees):
|
||||
"""
|
||||
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
|
||||
|
||||
Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
|
||||
construction methods used here are different enough that they should be separate.
|
||||
|
||||
At a high level, the steps followed ar:
|
||||
(1) accept a set of wires
|
||||
(2) create another set of wires like this one, but which are transformed and rotated
|
||||
(3) create a ruledSurface between the sets of wires
|
||||
(40 create a shell and compute the resulting object
|
||||
|
||||
:param outerWire: the outermost wire, a cad.Wire
|
||||
:param innerWires: a list of inner wires, a list of cad.Wire
|
||||
:param vecCenter: the center point about which to rotate. the axis of rotation is defined by
|
||||
vecNormal, located at vecCenter. ( a cad.Vector )
|
||||
:param vecNormal: a vector along which to extrude the wires ( a cad.Vector )
|
||||
:param angleDegrees: the angle to rotate through while extruding
|
||||
:return: a cad.Solid object
|
||||
"""
|
||||
|
||||
#from this point down we are dealing with FreeCAD wires not cad.wires
|
||||
startWires = [outerWire.wrapped] + [ i.wrapped for i in innerWires]
|
||||
endWires = []
|
||||
p1 = vecCenter.wrapped
|
||||
p2 = vecCenter.add(vecNormal).wrapped
|
||||
|
||||
#make translated and rotated copy of each wire
|
||||
for w in startWires:
|
||||
w2 = w.copy()
|
||||
w2.translate(vecNormal.wrapped)
|
||||
w2.rotate(p1,p2,angleDegrees)
|
||||
endWires.append(w2)
|
||||
|
||||
#make a ruled surface for each set of wires
|
||||
sides = []
|
||||
for w1,w2 in zip(startWires,endWires):
|
||||
rs = Part.makeRuledSurface(w1,w2)
|
||||
sides.append(rs)
|
||||
|
||||
#make faces for the top and bottom
|
||||
startFace = Part.Face(startWires)
|
||||
endFace = Part.Face(endWires)
|
||||
|
||||
#collect all the faces from the sides
|
||||
faceList = [ startFace]
|
||||
for s in sides:
|
||||
faceList.extend(s.Faces)
|
||||
faceList.append(endFace)
|
||||
|
||||
shell = Part.makeShell(faceList)
|
||||
solid = Part.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 = Part.Face(freeCADWires)
|
||||
result = f.extrude(vecNormal.wrapped)
|
||||
|
||||
return Shape.cast(result)
|
||||
|
||||
def tessellate(self,tolerance):
|
||||
return self.wrapped.tessellate(tolerance)
|
||||
|
||||
def intersect(self,toIntersect):
|
||||
"""
|
||||
computes the intersection between this solid and the supplied one
|
||||
The result could be a face or a compound of faces
|
||||
"""
|
||||
return Shape.cast(self.wrapped.common(toIntersect.wrapped))
|
||||
|
||||
def cut(self,solidToCut):
|
||||
"Remove a solid from another one"
|
||||
return Shape.cast(self.wrapped.cut(solidToCut.wrapped))
|
||||
|
||||
def fuse(self,solidToJoin):
|
||||
return Shape.cast(self.wrapped.fuse(solidToJoin.wrapped))
|
||||
|
||||
def fillet(self,radius,edgeList):
|
||||
"""
|
||||
Fillets the specified edges of this solid.
|
||||
:param radius: float > 0, the radius of the fillet
|
||||
:param edgeList: a list of Edge objects, which must belong to this solid
|
||||
:return: Filleted solid
|
||||
"""
|
||||
nativeEdges = [e.wrapped for e in edgeList]
|
||||
return Shape.cast(self.wrapped.makeFillet(radius,nativeEdges))
|
||||
|
||||
def shell(self,faceList,thickness,tolerance=0.0001):
|
||||
"""
|
||||
make a shelled solid of given by removing the list of faces
|
||||
|
||||
:param faceList: list of face objects, which must be part of the solid.
|
||||
:param thickness: floating point thickness. positive shells outwards, negative shells inwards
|
||||
:param tolerance: modelling tolerance of the method, default=0.0001
|
||||
:return: a shelled solid
|
||||
|
||||
**WARNING** The underlying FreeCAD implementation can very frequently have problems
|
||||
with shelling complex geometries!
|
||||
"""
|
||||
nativeFaces = [ f.wrapped for f in faceList]
|
||||
return Shape.cast( self.wrapped.makeThickness(nativeFaces,thickness,tolerance))
|
||||
|
||||
class Compound(Shape):
|
||||
def __init__(self,obj):
|
||||
"""
|
||||
An Edge
|
||||
"""
|
||||
self.wrapped = obj
|
||||
|
||||
def Center(self):
|
||||
#TODO: compute the weighted average instead of the first solid
|
||||
return self.Solids()[0].Center()
|
||||
|
||||
@classmethod
|
||||
def makeCompound(cls,listOfShapes):
|
||||
"""
|
||||
Create a compound out of a list of shapes
|
||||
"""
|
||||
solids = [s.wrapped for s in listOfShapes]
|
||||
c = Part.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)
|
4
cadquery/plugins/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .CQ import CQ
|
||||
from .workplane import Workplane
|
||||
|
||||
__version__ = 0.9
|
175
cadquery/plugins/gears.py
Normal file
@ -0,0 +1,175 @@
|
||||
__author__ = 'dcowden'
|
||||
"""
|
||||
Utilities exposed to users
|
||||
"""
|
||||
import math
|
||||
|
||||
# =================================================================================
|
||||
# =================================================================================
|
||||
# Spur-gear generation script
|
||||
# (c) James Gregson, 2012
|
||||
# Free for all use, including commercial, but do not redistribute.
|
||||
# Use at your own risk.
|
||||
#
|
||||
# Notes:
|
||||
# - seems to work well for pressure angles up to about 30 degrees
|
||||
# =================================================================================
|
||||
# =================================================================================
|
||||
|
||||
# compute the root diameter of a gear with a given pressure-angle (pa)
|
||||
# number of teeth (N), and pitch (P)
|
||||
def gears_root_diameter( pa, N, P ):
|
||||
return (N-2.5)/P
|
||||
|
||||
# compute the base diameter of a gear with a given pressure-angle (pa)
|
||||
# number of teeth (N), and pitch (P)
|
||||
def gears_base_diameter( pa, N, P ):
|
||||
return gears_pitch_diameter( pa, N, P )*math.cos( pa*math.pi/180.0 )
|
||||
|
||||
# compute the outer diameter of a gear with a given pressure-angle (pa)
|
||||
# number of teeth (N), and pitch (P)
|
||||
def gears_outer_diameter( pa, N, P ):
|
||||
return gears_pitch_diameter( pa, N, P ) + 2.0*gears_addendum( pa, N, P )
|
||||
|
||||
# compute the outer diameter of a gear with a given pressure-angle (pa)
|
||||
# number of teeth (N), and pitch (P)
|
||||
def gears_pitch_diameter( pa, N, P ):
|
||||
return float(N)/float(P)
|
||||
|
||||
# compute the outer diameter of a gear with a given pressure-angle (pa)
|
||||
# number of teeth (N) and pitch (P)
|
||||
def gears_circular_pitch( pa, N, P ):
|
||||
return math.pi/float(P)
|
||||
|
||||
# compute the circular tooth thickness of a gear with a given
|
||||
# pressure-angle (pa), number of teeth (N) and pitch (P)
|
||||
def gears_circular_tooth_thickness( pa, N, P, backlash=0.05 ):
|
||||
return gears_circular_pitch( pa, N, P )/(2.0+backlash)
|
||||
|
||||
# compute the circular tooth angle of a gear with a given
|
||||
# pressure-angle (pa), number of teeth (N) and pitch (P)
|
||||
def gears_circular_tooth_angle( pa, N, P ):
|
||||
return gears_circular_tooth_thickness( pa, N, P )*2.0/gears_pitch_diameter( pa, N, P )
|
||||
|
||||
# compute the addendum height for a gear with a given
|
||||
# pressure-angle (pa), number of teeth (N) and pitch (P)
|
||||
def gears_addendum( pa, N, P ):
|
||||
return 1.0/float(P)
|
||||
|
||||
# compute the dedendum depth for a gear with a given
|
||||
# pressur-angle (pa), number of teeth (N) and pitch (P)
|
||||
def gears_dedendum( pa, N, P ):
|
||||
return 1.25/float(P)
|
||||
|
||||
# generates an involute curve from a circle of radius r up to theta_max radians
|
||||
# with a specified number of steps
|
||||
def gears_generate_involute( r, r_max, theta_max, steps=30 ):
|
||||
dtheta = theta_max / float(steps)
|
||||
x = []
|
||||
y = []
|
||||
theta = []
|
||||
rlast = r;
|
||||
for i in range( 0, steps+1 ):
|
||||
c = math.cos( i*dtheta )
|
||||
s = math.sin( i*dtheta )
|
||||
tx = r*( c + i*dtheta*s )
|
||||
ty = r*( s - i*dtheta*c )
|
||||
d = math.sqrt(tx*tx+ty*ty)
|
||||
if d > r_max:
|
||||
a = (r_max-rlast)/(d-rlast)
|
||||
tx = x[-1]*(1.0-a) + tx*a
|
||||
ty = y[-1]*(1.0-a) + ty*a
|
||||
ttheta = theta[-1]*(1.0-a) + math.atan2( ty, tx )*a
|
||||
x.append( tx )
|
||||
y.append( ty )
|
||||
theta.append( ttheta )
|
||||
break
|
||||
else:
|
||||
x.append( tx )
|
||||
y.append( ty )
|
||||
theta.append( math.atan2( ty, tx) )
|
||||
return x, y, theta
|
||||
|
||||
# returns the angle where an involute curve crosses a circle with a given radius
|
||||
# or -1 on failure
|
||||
def gears_locate_involute_cross_angle_for_radius( r, ix, iy, itheta ):
|
||||
for i in range( 0, len(ix)-1 ):
|
||||
r2 = ix[i+1]*ix[i+1] + iy[i+1]*iy[i+1]
|
||||
if r2 > r*r:
|
||||
r1 = math.sqrt( ix[i]*ix[i] + iy[i]*iy[i] )
|
||||
r2 = math.sqrt( r2 )
|
||||
a = (r-r1)/(r2-r1)
|
||||
return itheta[i]*(1.0-a) + itheta[i+1]*a
|
||||
return -1.0
|
||||
|
||||
# rotates the involute curve around the gear center in order to have the involute
|
||||
# cross the x-axis at the pitch diameter
|
||||
def gears_align_involute( Dp, ix, iy, itheta ):
|
||||
theta = -gears_locate_involute_cross_angle_for_radius( Dp/2.0, ix, iy, itheta )
|
||||
c = math.cos(theta)
|
||||
s = math.sin(theta)
|
||||
for i in range( 0, len(ix) ):
|
||||
tx = c*ix[i] - s*iy[i]
|
||||
ty = s*ix[i] + c*iy[i]
|
||||
ix[i] = tx
|
||||
iy[i] = ty
|
||||
return ix, iy
|
||||
|
||||
# reflects the input curve about the x-axis to generate the opposing face of
|
||||
# a tooth
|
||||
def gears_mirror_involute( ix, iy ):
|
||||
tx = []
|
||||
ty = []
|
||||
for i in range( 0, len(iy) ):
|
||||
tx.append( ix[len(iy)-1-i] )
|
||||
ty.append( -iy[len(iy)-1-i] )
|
||||
return tx, ty
|
||||
|
||||
# rotates the input curve by a given angle (in radians)
|
||||
def gears_rotate( theta, ix, iy ):
|
||||
c = math.cos(theta)
|
||||
s = math.sin(theta)
|
||||
x = []
|
||||
y = []
|
||||
for i in range( 0, len(ix) ):
|
||||
tx = c*ix[i] - s*iy[i]
|
||||
ty = s*ix[i] + c*iy[i]
|
||||
x.append( tx )
|
||||
y.append( ty )
|
||||
return x, y
|
||||
|
||||
# translates the input curve by [dx, dy]
|
||||
def gears_translate( dx, dy, ix, iy ):
|
||||
x = []
|
||||
y = []
|
||||
for i in range( 0, len(ix) ):
|
||||
x.append( ix[i]+dx )
|
||||
y.append( iy[i]+dy )
|
||||
return x, y
|
||||
|
||||
# generates a single tooth profile of a spur gear
|
||||
def gears_make_tooth( pa, N, P ):
|
||||
ix, iy, itheta = gears_generate_involute( gears_base_diameter( pa, N, P )/2.0, gears_outer_diameter( pa, N, P )/2.0, math.pi/2.1 )
|
||||
ix.insert( 0, min( gears_base_diameter( pa, N, P )/2.0, gears_root_diameter( pa, N, P )/2.0 ) )
|
||||
iy.insert( 0, 0.0 )
|
||||
itheta.insert( 0, 0.0 )
|
||||
ix, iy = gears_align_involute( gears_pitch_diameter(pa, N, P), ix, iy, itheta )
|
||||
mx, my = gears_mirror_involute( ix, iy )
|
||||
mx, my = gears_rotate( gears_circular_tooth_angle( pa, N, P ), mx, my )
|
||||
ix.extend( mx )
|
||||
iy.extend( my )
|
||||
return ix, iy
|
||||
|
||||
# generates a spur gear with a given pressure angle (pa),
|
||||
# number of teeth (N) and pitch (P)
|
||||
def make_gear( pa, N, P ):
|
||||
tx, ty = gears_make_tooth( pa, N, P )
|
||||
x = []
|
||||
y = []
|
||||
for i in range( 0, N ):
|
||||
rx, ry = gears_rotate( float(i)*2.0*math.pi/float(N), tx, ty )
|
||||
x.extend( rx )
|
||||
y.extend( ry )
|
||||
x.append( x[0] )
|
||||
y.append( y[0] )
|
||||
return zip(x, y)
|
350
cadquery/selectors.py
Normal file
@ -0,0 +1,350 @@
|
||||
"""
|
||||
A Parametric CAD System for the Web
|
||||
Copyright (c) 2010-2022 Parametric Products Intellectual Holdings LLC, All Rights Reserved.
|
||||
|
||||
Objects that select geometry
|
||||
"""
|
||||
|
||||
import re
|
||||
import math
|
||||
|
||||
class Selector(object):
|
||||
"""
|
||||
Filters a list of objects
|
||||
|
||||
Filters must provide a single method that filters objects.
|
||||
"""
|
||||
def filter(self,objectList):
|
||||
"""
|
||||
Filter the provided list
|
||||
:param objectList: list to filter
|
||||
:type objectList: list of FreeCAD primatives
|
||||
:return: filtered list
|
||||
|
||||
The default implementation returns the original list unfiltered
|
||||
|
||||
"""
|
||||
return objectList
|
||||
|
||||
class NearestToPointSelector(Selector):
|
||||
"""
|
||||
Selects object nearest the provided point.
|
||||
|
||||
If the object is a vertex or point, the distance
|
||||
is used. For other kinds of shapes, the center of mass
|
||||
is used to to compute which is closest.
|
||||
|
||||
Applicability: All Types of Shapes
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).vertices(NearestToPointSelector((0,1,0))
|
||||
|
||||
returns the vertex of the unit cube closest to the point x=0,y=1,z=0
|
||||
|
||||
"""
|
||||
def __init__(self,pnt ):
|
||||
self.pnt = pnt
|
||||
def filter(self,objectList):
|
||||
|
||||
def dist(tShape):
|
||||
return tShape.Center().sub(self.pnt).Length
|
||||
#if tShape.ShapeType == 'Vertex':
|
||||
# return tShape.Point.sub(toVector(self.pnt)).Length
|
||||
#else:
|
||||
# return tShape.CenterOfMass.sub(toVector(self.pnt)).Length
|
||||
|
||||
return [ min(objectList,key=dist) ]
|
||||
|
||||
|
||||
class BaseDirSelector(Selector):
|
||||
"""
|
||||
A selector that handles selection on the basis of a single
|
||||
direction vector
|
||||
"""
|
||||
def __init__(self,vector,tolerance=0.0001 ):
|
||||
self.direction = vector
|
||||
self.TOLERANCE = tolerance
|
||||
|
||||
def test(self,vec):
|
||||
"Test a specified vector. Subclasses override to provide other implementations"
|
||||
return True
|
||||
|
||||
def filter(self,objectList):
|
||||
"""
|
||||
There are lots of kinds of filters, but
|
||||
for planes they are always based on the normal of the plane,
|
||||
and for edges on the tangent vector along the edge
|
||||
"""
|
||||
r = []
|
||||
for o in objectList:
|
||||
#no really good way to avoid a switch here, edges and faces are simply different!
|
||||
|
||||
if type(o) == Face:
|
||||
# a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir
|
||||
normal = o.normalAt(None)
|
||||
|
||||
if self.test(normal):
|
||||
r.append(o)
|
||||
elif type(o) == Edge and o.geomType() == 'LINE':
|
||||
#an edge is parallel to a direction if it is a line, and the line is parallel to the dir
|
||||
tangent = o.tangentAt(None)
|
||||
if self.test(tangent):
|
||||
r.append(o)
|
||||
|
||||
return r
|
||||
|
||||
class ParallelDirSelector(BaseDirSelector):
|
||||
"""
|
||||
Selects objects parallel with the provided direction
|
||||
|
||||
Applicability:
|
||||
Linear Edges
|
||||
Planar Faces
|
||||
|
||||
Use the string syntax shortcut \|(X|Y|Z) if you want to select
|
||||
based on a cardinal direction.
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).faces(ParallelDirSelector((0,0,1))
|
||||
|
||||
selects faces with a normals in the z direction, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces("|Z")
|
||||
"""
|
||||
|
||||
def test(self,vec):
|
||||
return self.direction.cross(vec).Length < self.TOLERANCE
|
||||
|
||||
class DirectionSelector(BaseDirSelector):
|
||||
"""
|
||||
Selects objects aligned with the provided direction
|
||||
|
||||
Applicability:
|
||||
Linear Edges
|
||||
Planar Faces
|
||||
|
||||
Use the string syntax shortcut +/-(X|Y|Z) if you want to select
|
||||
based on a cardinal direction.
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).faces(DirectionSelector((0,0,1))
|
||||
|
||||
selects faces with a normals in the z direction, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces("+Z")
|
||||
"""
|
||||
|
||||
def test(self,vec):
|
||||
return abs(self.direction.getAngle(vec) < self.TOLERANCE)
|
||||
|
||||
class PerpendicularDirSelector(BaseDirSelector):
|
||||
"""
|
||||
Selects objects perpendicular with the provided direction
|
||||
|
||||
Applicability:
|
||||
Linear Edges
|
||||
Planar Faces
|
||||
|
||||
Use the string syntax shortcut #(X|Y|Z) if you want to select
|
||||
based on a cardinal direction.
|
||||
|
||||
Example::
|
||||
|
||||
CQ(aCube).faces(PerpendicularDirSelector((0,0,1))
|
||||
|
||||
selects faces with a normals perpendicular to the z direction, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces("#Z")
|
||||
"""
|
||||
|
||||
def test(self,vec):
|
||||
angle = self.direction.getAngle(vec)
|
||||
r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE )
|
||||
return not r
|
||||
|
||||
|
||||
class TypeSelector(Selector):
|
||||
"""
|
||||
Selects objects of the prescribed topological type.
|
||||
|
||||
Applicability:
|
||||
Faces: Plane,Cylinder,Sphere
|
||||
Edges: Line,Circle,Arc
|
||||
|
||||
You can use the shortcut selector %(PLANE|SPHERE|CONE) for faces,
|
||||
and %(LINE|ARC|CIRCLE) for edges.
|
||||
|
||||
For example this::
|
||||
|
||||
CQ(aCube).faces ( TypeSelector("PLANE") )
|
||||
|
||||
will select 6 faces, and is equivalent to::
|
||||
|
||||
CQ(aCube).faces( "%PLANE" )
|
||||
|
||||
"""
|
||||
def __init__(self,typeString):
|
||||
self.typeString = typeString.upper()
|
||||
|
||||
def filter(self,objectList):
|
||||
r = []
|
||||
for o in objectList:
|
||||
if o.geomType() == self.typeString:
|
||||
r.append(o)
|
||||
return r
|
||||
|
||||
class DirectionMinMaxSelector(Selector):
|
||||
"""
|
||||
Selects objects closest or farthest in the specified direction
|
||||
Used for faces, points, and edges
|
||||
|
||||
Applicability:
|
||||
All object types. for a vertex, its point is used. for all other kinds
|
||||
of objects, the center of mass of the object is used.
|
||||
|
||||
You can use the string shortcuts >(X|Y|Z) or <(X|Y|Z) if you want to
|
||||
select based on a cardinal direction.
|
||||
|
||||
For example this::
|
||||
|
||||
CQ(aCube).faces ( DirectionMinMaxSelector((0,0,1),True )
|
||||
|
||||
Means to select the face having the center of mass farthest in the positive z direction,
|
||||
and is the same as:
|
||||
|
||||
CQ(aCube).faces( ">Z" )
|
||||
|
||||
Future Enhancements:
|
||||
provide a nicer way to select in arbitrary directions. IE, a bit more code could
|
||||
allow '>(0,0,1)' to work.
|
||||
|
||||
"""
|
||||
def __init__(self,vector,directionMax=True):
|
||||
self.vector = vector
|
||||
self.max = max
|
||||
self.directionMax = directionMax
|
||||
def filter(self,objectList):
|
||||
|
||||
#then sort by distance from origin, along direction specified
|
||||
def distance(tShape):
|
||||
return tShape.Center().dot(self.vector)
|
||||
#if tShape.ShapeType == 'Vertex':
|
||||
# pnt = tShape.Point
|
||||
#else:
|
||||
# pnt = tShape.Center()
|
||||
#return pnt.dot(self.vector)
|
||||
|
||||
if self.directionMax:
|
||||
return [ max(objectList,key=distance) ]
|
||||
else:
|
||||
return [ min(objectList,key=distance) ]
|
||||
|
||||
|
||||
class StringSyntaxSelector(Selector):
|
||||
"""
|
||||
Filter lists objects using a simple string syntax. All of the filters available in the string syntax
|
||||
are also available ( usually with more functionality ) through the creation of full-fledged
|
||||
selector objects. see :py:class:`Selector` and its subclasses
|
||||
|
||||
Filtering works differently depending on the type of object list being filtered.
|
||||
|
||||
:param selectorString: A two-part selector string, [selector][axis]
|
||||
|
||||
:return: objects that match the specified selector
|
||||
|
||||
***Modfiers*** are ``('|','+','-','<','>','%')``
|
||||
|
||||
:\|:
|
||||
parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects.
|
||||
:#:
|
||||
perpendicular to (same as :py:class:`PerpendicularDirSelector` )
|
||||
:+:
|
||||
positive direction (same as :py:class:`DirectionSelector` )
|
||||
:-:
|
||||
negative direction (same as :py:class:`DirectionSelector` )
|
||||
:>:
|
||||
maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True)
|
||||
:<:
|
||||
minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False )
|
||||
:%:
|
||||
curve/surface type (same as :py:class:`TypeSelector`)
|
||||
|
||||
***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ``
|
||||
|
||||
Selectors are a complex topic: see :ref:`selector_reference` for more information
|
||||
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self,selectorString):
|
||||
|
||||
self.axes = {
|
||||
'X': Vector(1,0,0),
|
||||
'Y': Vector(0,1,0),
|
||||
'Z': Vector(0,0,1),
|
||||
'XY': Vector(1,1,0),
|
||||
'YZ': Vector(0,1,1),
|
||||
'XZ': Vector(1,0,1)
|
||||
}
|
||||
|
||||
namedViews = {
|
||||
'front': ('>','Z' ),
|
||||
'back': ('<','Z'),
|
||||
'left':('<', 'X'),
|
||||
'right': ('>', 'X'),
|
||||
'top': ('>','Y'),
|
||||
'bottom': ('<','Y')
|
||||
}
|
||||
self.selectorString = selectorString
|
||||
r = re.compile("\s*([-\+<>\|\%#])*\s*(\w+)\s*",re.IGNORECASE)
|
||||
m = r.match(selectorString)
|
||||
|
||||
if m != None:
|
||||
if namedViews.has_key(selectorString):
|
||||
(a,b) = namedViews[selectorString]
|
||||
self.mySelector = self._chooseSelector(a,b )
|
||||
else:
|
||||
self.mySelector = self._chooseSelector(m.groups()[0],m.groups()[1])
|
||||
else:
|
||||
raise ValueError ("Selector String format must be [-+<>|#%] X|Y|Z ")
|
||||
|
||||
|
||||
def _chooseSelector(self,selType,selAxis):
|
||||
"""Sets up the underlying filters accordingly"""
|
||||
|
||||
if selType == "%":
|
||||
return TypeSelector(selAxis)
|
||||
|
||||
#all other types need to select axis as a vector
|
||||
#get the axis vector first, will throw an except if an unknown axis is used
|
||||
try:
|
||||
vec = self.axes[selAxis]
|
||||
except KeyError:
|
||||
raise ValueError ("Axis value %s not allowed: must be one of %s" % (selAxis, str(self.axes)))
|
||||
|
||||
if selType in (None, "+"):
|
||||
#use direction filter
|
||||
return DirectionSelector(vec)
|
||||
elif selType == '-':
|
||||
#just use the reverse of the direction vector
|
||||
return DirectionSelector(vec.multiply(-1.0))
|
||||
elif selType == "|":
|
||||
return ParallelDirSelector(vec)
|
||||
elif selType == ">":
|
||||
return DirectionMinMaxSelector(vec,True)
|
||||
elif selType == "<":
|
||||
return DirectionMinMaxSelector(vec,False)
|
||||
elif selType == '#':
|
||||
return PerpendicularDirSelector(vec)
|
||||
else:
|
||||
raise ValueError ("Selector String format must be [-+<>|] X|Y|Z ")
|
||||
|
||||
def filter(self,objectList):
|
||||
"""
|
||||
selects minimum, maximum, positive or negative values relative to a direction
|
||||
[+\|-\|<\|>\|] \<X\|Y\|Z>
|
||||
"""
|
||||
return self.mySelector.filter(objectList)
|
1412
cadquery/workplane.py
Normal file
0
changes.md
Normal file
0
requirements.txt
Normal file
153
sphinxdoc/Makefile
Normal file
@ -0,0 +1,153 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/CadQuery.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/CadQuery.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/CadQuery"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/CadQuery"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
BIN
sphinxdoc/_static/ParametricPulley.PNG
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
sphinxdoc/_static/PillowBlock.PNG
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
sphinxdoc/_static/block.png
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
sphinxdoc/_static/new_badge.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
sphinxdoc/_static/parametric-cup-screencap.PNG
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
sphinxdoc/_static/quickstart-1.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
sphinxdoc/_static/quickstart-2.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
sphinxdoc/_static/quickstart-3.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
sphinxdoc/_static/quickstart-4.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
sphinxdoc/_static/quickstart-5.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
sphinxdoc/_static/quickstart.png
Normal file
After Width: | Height: | Size: 18 KiB |
139
sphinxdoc/apireference.rst
Normal file
@ -0,0 +1,139 @@
|
||||
.. _apireference:
|
||||
|
||||
***********************
|
||||
CadQuery API Reference
|
||||
***********************
|
||||
|
||||
.. automodule:: cadfile.cadutils.cadquery
|
||||
|
||||
.. seealso::
|
||||
This page lists api methods grouped by functional area.
|
||||
Use :ref:`classreference` to see methods alphabetically by class.
|
||||
Don't see a method you want? see :ref:`extending`
|
||||
|
||||
Primary Objects
|
||||
----------------
|
||||
|
||||
The CadQuery API is made up of 3 main objects:
|
||||
|
||||
* **CQ** - Basic Selection, and 3d operations
|
||||
* **Workplane** -- Draw in 2-d to make 3d features
|
||||
* **Selector** -- Filter and select things
|
||||
|
||||
The sections below list methods of these objects grouped by **functional area**
|
||||
|
||||
Initialization
|
||||
----------------
|
||||
|
||||
Creating new workplanes and object chains
|
||||
|
||||
.. autosummary::
|
||||
CQ
|
||||
Workplane
|
||||
CQ.workplane
|
||||
|
||||
|
||||
.. _2dOperations:
|
||||
|
||||
2-d Operations
|
||||
-----------------
|
||||
|
||||
Creating 2-d constructs that can be used to create 3 d features
|
||||
|
||||
.. autosummary::
|
||||
Workplane.center
|
||||
Workplane.lineTo
|
||||
Workplane.line
|
||||
Workplane.vLine
|
||||
Workplane.vLineTo
|
||||
Workplane.hLine
|
||||
Workplane.moveTo
|
||||
Workplane.move
|
||||
Workplane.spline
|
||||
Workplane.threePointArc
|
||||
Workplane.rotateAndCopy
|
||||
Workplane.mirrorY
|
||||
Workplane.mirrorX
|
||||
Workplane.wire
|
||||
Workplane.rect
|
||||
Workplane.circle
|
||||
Workplane.polyline
|
||||
Workplane.close
|
||||
Workplane.rarray
|
||||
|
||||
.. _3doperations:
|
||||
|
||||
3-d Operations
|
||||
-----------------
|
||||
|
||||
Methods that create 3d features
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Workplane.cboreHole
|
||||
Workplane.cskHole
|
||||
Workplane.hole
|
||||
Workplane.extrude
|
||||
Workplane.cut
|
||||
Workplane.cutBlind
|
||||
Workplane.cutThruAll
|
||||
Workplane.box
|
||||
Workplane.union
|
||||
Workplane.combine
|
||||
CQ.shell
|
||||
CQ.fillet
|
||||
CQ.split
|
||||
CQ.rotateAboutCenter
|
||||
CQ.translate
|
||||
|
||||
|
||||
Iteration Methods
|
||||
------------------
|
||||
|
||||
Methods that allow iteration over the stack or objects
|
||||
|
||||
.. autosummary::
|
||||
Workplane.each
|
||||
Workplane.eachpoint
|
||||
|
||||
|
||||
.. _stackMethods:
|
||||
|
||||
Stack Methods
|
||||
-----------------
|
||||
|
||||
CadQuery methods that operate on the stack
|
||||
|
||||
.. autosummary::
|
||||
CQ.all
|
||||
CQ.size
|
||||
CQ.vals
|
||||
CQ.add
|
||||
CQ.val
|
||||
CQ.first
|
||||
CQ.item
|
||||
CQ.last
|
||||
CQ.end
|
||||
CQ.vertices
|
||||
CQ.faces
|
||||
CQ.edges
|
||||
CQ.wires
|
||||
CQ.solids
|
||||
CQ.shells
|
||||
CQ.compounds
|
||||
|
||||
.. _selectors:
|
||||
|
||||
Selectors
|
||||
------------------------
|
||||
|
||||
Objects that filter and select CAD objects
|
||||
|
||||
.. autosummary::
|
||||
NearestToPointSelector
|
||||
ParallelDirSelector
|
||||
DirectionSelector
|
||||
PerpendicularDirSelector
|
||||
TypeSelector
|
||||
DirectionMinMaxSelector
|
||||
StringSyntaxSelector
|
243
sphinxdoc/cadquerybasics.rst
Normal file
@ -0,0 +1,243 @@
|
||||
.. _cadquerybasics:
|
||||
|
||||
.. automodule:: cadfile.cadutils.cadquery
|
||||
|
||||
*************************
|
||||
Introduction to CadQuery
|
||||
*************************
|
||||
|
||||
This page describes basic CadQuery concepts and goals. CadQuery is still under development, but already offers a lot.
|
||||
|
||||
======================
|
||||
Goals and Principles
|
||||
======================
|
||||
|
||||
|
||||
Principle 1: Intuitive Construction
|
||||
====================================
|
||||
|
||||
CadQuery aims to make building models using python scripting easy and intuitive.
|
||||
CadQuery strives to allow scripts to read roughly as a human would describe an object verbally.
|
||||
|
||||
For example, consider this object:
|
||||
|
||||
.. image:: quickstart.png
|
||||
|
||||
A human would describe this as:
|
||||
|
||||
"A block 80mm square x 30mm thick , with countersunk holes for M2 socket head cap screws
|
||||
at the corners, and a circular pocket 22mm in diameter in the middle for a bearing"
|
||||
|
||||
The goal is to have the CadQuery script that produces this object be as close as possible to the english phrase
|
||||
a human would use.
|
||||
|
||||
|
||||
Principle 2: Capture Design Intent
|
||||
====================================
|
||||
|
||||
The features that are **not** part of the part description above are just as important as those that are. For example, most
|
||||
humans will assume that:
|
||||
|
||||
* The countersunk holes are spaced a uniform distance from the edges
|
||||
* The circular pocket is in the center of the block, no matter how big the block is
|
||||
|
||||
If you have experience with 3D CAD systems, you also know that there is a key design intent built into this object.
|
||||
After the base block is created, how the hole is located is key. If it is located from one edge, changing the block
|
||||
size will have a different affect than if the hole is located from the center.
|
||||
|
||||
Many scripting langauges to not provide a way to capture design intent-- because they require that you always work in
|
||||
global coordinates. CadQuery is different-- you can locate features relative to others in a relative way-- preserving
|
||||
the design intent just like a human would when creating a drawing or building an object.
|
||||
|
||||
In fact, though many people know how to use 3D CAD systems, few understand how important the way that an object is built
|
||||
impact its maintainability and resiliency to design changes.
|
||||
|
||||
|
||||
Principle 3: Plugins as first class citizens
|
||||
============================================
|
||||
|
||||
Any system for building 3D models will evolve to contain an immense number of libraries and feature builders. It is
|
||||
important that these can be seamlessly included into the core and used alongside the built in libraries. Plugins
|
||||
should be easy to install and familiar to use.
|
||||
|
||||
|
||||
Principle 4: CAD models as source code makes sense
|
||||
==================================================================
|
||||
|
||||
It is surprising that the world of 3D CAD is primarily dominated by systems that create opaque binary files.
|
||||
Just like the world of software, CAD models are very complex.
|
||||
|
||||
CAD models have many things in common with software, and would benefit greatly from the use of tools that are standard
|
||||
in the software industry, such as:
|
||||
|
||||
1. Easily re-using features between objects
|
||||
2. Storing objects using version control systems
|
||||
3. Computing the differences between objects by using source control tools
|
||||
4. Share objects on the internet
|
||||
5. Automate testing and generation by allowing objects to be built from within libraries
|
||||
|
||||
CadQuery is designed to make 3D content creation easy enough that the above benefits can be attained without more work
|
||||
than using existing 'opaque', 'point and click' solutions.
|
||||
|
||||
======================
|
||||
3D Topology Primer
|
||||
======================
|
||||
|
||||
Before talking about CadQuery, it makes sense to talk a little about 3D CAD Topology. CadQuery is based upon the
|
||||
OpenCascade kernel, which is uses Boundary Representations ( BREP ) for objects. This just means that objects
|
||||
are defined by their enclosing surfaces.
|
||||
|
||||
When working in a BREP system, these fundamental constructs exist to define a shape ( working up the food chain):
|
||||
|
||||
:vertex: a single point in space
|
||||
:edge: a connection between two or more vertices along a particular path ( called a curve )
|
||||
:wire: a collection of edges that are connected together.
|
||||
:face: a set of edges or wires that enclose a surface
|
||||
:shell: a collection of faces that are connected together along some of their edges
|
||||
:solid: a shell that has a closed interior
|
||||
:compound: a collection of solids
|
||||
|
||||
When using CadQuery, all of these objects are created, hopefully with the least possible work. In the actual CAD
|
||||
kernel, there are another set of Geometrical constructs involved as well. For example, an arc-shaped edge will
|
||||
hold a reference to an underlying curve that is a full cricle, and each linear edge holds underneath it the equation
|
||||
for a line. CadQuery shields you from these constructs.
|
||||
|
||||
======================
|
||||
CadQuery Concepts
|
||||
======================
|
||||
|
||||
CadQuery provides functions several key areas. As you would expect, many are devoted to easy creation of
|
||||
2D and 3D features. But just as many, if not more, are for navigating and selecting objects.
|
||||
|
||||
* CQ, the CadQuery object
|
||||
* Workplanes
|
||||
* Selection
|
||||
* 2D Construction
|
||||
* 3D Construction
|
||||
* construction geometry
|
||||
* easy iteration
|
||||
|
||||
|
||||
CQ, the CadQuery Object
|
||||
========================
|
||||
|
||||
The CadQuery object wraps a BREP feature, and provides functionality around it. Typical examples include rotating,
|
||||
transforming, combining objects, and creating workplanes.
|
||||
|
||||
See :ref:`apireference` to learn more.
|
||||
|
||||
|
||||
Workplanes
|
||||
======================
|
||||
|
||||
Workplanes represent a plane in space, from which other features can be located. They have a center point and a local
|
||||
coordinate system.
|
||||
|
||||
The most common way to create a workplane is to locate one on the face of a solid. You can also create new workplanes
|
||||
in space, or relative to other planes using offsets or rotations.
|
||||
|
||||
The most powerful feature of workplanes is that they allow you to work in 2D space in the coordinate system of the
|
||||
workplane, and then build 3D features based on local coordinates. This makes scripts much easier to create and maintain.
|
||||
|
||||
See :py:class:`Workplane` to learn more
|
||||
|
||||
|
||||
2D Construction
|
||||
======================
|
||||
|
||||
Once you create a workplane, you can work in 2D, and then later use the features you create to make 3D objects.
|
||||
You'll find all of the 2D constructs you expect-- circles, lines, arcs, mirroring, points, etc.
|
||||
|
||||
See :ref:`2dOperations` to learn more.
|
||||
|
||||
|
||||
3D Construction
|
||||
======================
|
||||
|
||||
You can construct 3D primatives such as boxes, spheres, wedges, and cylinders directly. You can also sweep, extrude,
|
||||
and loft 2D geometry to form 3D features. Of course the basic primitive operations are also available.
|
||||
|
||||
See :ref:`3doperations` to learn more.
|
||||
|
||||
|
||||
|
||||
Selectors
|
||||
======================
|
||||
|
||||
Selectors allow you to select one or more features, for use to define new features. As an example, you might
|
||||
extrude a box, and then select the top face as the location for a new feture. Or, you might extrude a box, and
|
||||
then select all of the vertical edges so that you can apply a fillet to them.
|
||||
|
||||
You can select Vertices, Edges, Faces, Solids, and Wires using selectors.
|
||||
|
||||
Think of selectors as the equivalent of your hand and mouse, were you to build an object using a conventional CAD system.
|
||||
|
||||
You can learn more about selectors :ref:`selectors`
|
||||
|
||||
|
||||
Construction Geometry
|
||||
======================
|
||||
|
||||
Construction geometry are features that are not part of the object, but are only defined to aid in building the object.
|
||||
A common example might be to define a rectangle, and then use the corners to define a the location of a set of holes.
|
||||
|
||||
Most CadQuery construction methods provide a forConstruction keyword, which creates a feature that will only be used
|
||||
to locate other features
|
||||
|
||||
|
||||
The Stack
|
||||
======================
|
||||
|
||||
As you work in CadQuery, each operation returns a new CadQuery object with the result of that operations. Each CadQuery
|
||||
object has a list of objects, and a reference to its parent.
|
||||
|
||||
You can always go backwards to older operations by removing the current object from the stack. For example::
|
||||
|
||||
CQ(someObject).faces(">Z").first().vertices()
|
||||
|
||||
returns a CadQuery object that contains all of the vertices on highest face of someObject. But you can always move
|
||||
backwards in the stack to get the face as well::
|
||||
|
||||
CQ(someObject).faces(">Z").first().vertices().end() #returns the same as CQ(someObject).faces(">Z").first()
|
||||
|
||||
You can browse stack access methods here :ref:`stackMethods`
|
||||
|
||||
|
||||
Chaining
|
||||
======================
|
||||
|
||||
All CadQuery methods return another CadQuery object, so that you can chain the methods together fluently. Use
|
||||
the core CQ methods to get at the objects that were created.
|
||||
|
||||
|
||||
The Context Solid
|
||||
======================
|
||||
|
||||
Most of the time, you are building a single object, and adding features to that single object. CadQuery watches
|
||||
your operations, and defines the first solid object created as the 'context solid'. After that, any features
|
||||
you create are automatically combined ( unless you specify otherwise) with that solid. This happens even if the
|
||||
solid was created a long way up in the stack. For example::
|
||||
|
||||
Workplane('XY').box(1,2,3).faces(">Z").circle(0.25).extrude()
|
||||
|
||||
Will create a 1x2x3 box, with a cylindrical boss extending from the top face. It was not necessary to manually
|
||||
combine the cylinder created by extruding the circle with the box, because the default behavior for extrude is
|
||||
to combine the result with the context solid. The hole() method works similarly-- CadQuery presumes that you want
|
||||
to subtract the hole from the context solid.
|
||||
|
||||
If you want to avoid this, you can specified combine=False, and CadQuery will create the solid separately.
|
||||
|
||||
|
||||
Iteration
|
||||
======================
|
||||
|
||||
CAD models often have repeated geometry, and its really annoying to resort to for loops to construct features.
|
||||
Many CadQuery methods operate automatically on each element on the stack, so that you don't have to write loops.
|
||||
For example, this::
|
||||
|
||||
Workplane('XY').box(1,2,3).faces(">Z").vertices().circle(0.5)
|
||||
|
||||
Will actually create 4 circles, because vertices() selects 4 vertices of a rectangular face, and the circle() method
|
||||
iterates on each member of the stack.
|
||||
|
||||
This is really useful to remember when you author your own plugins. :py:meth:`Workplane.each` is useful for this purpose.
|
71
sphinxdoc/classreference.rst
Normal file
@ -0,0 +1,71 @@
|
||||
.. _classreference:
|
||||
|
||||
*************************
|
||||
CadQuery Class Reference
|
||||
*************************
|
||||
|
||||
This page documents all of the methods and functions of the CadQuery classes, organized alphabatically.
|
||||
|
||||
.. seealso::
|
||||
|
||||
For a listing organized by functional area, see the :ref:`apireference`
|
||||
|
||||
.. automodule:: cadfile.cadutils.cadquery
|
||||
|
||||
Core Classes
|
||||
---------------------
|
||||
|
||||
.. autosummary::
|
||||
CQ
|
||||
Plane
|
||||
Workplane
|
||||
|
||||
|
||||
Selectors
|
||||
---------------------
|
||||
|
||||
.. autosummary::
|
||||
NearestToPointSelector
|
||||
ParallelDirSelector
|
||||
DirectionSelector
|
||||
PerpendicularDirSelector
|
||||
TypeSelector
|
||||
DirectionMinMaxSelector
|
||||
StringSyntaxSelector
|
||||
|
||||
Classes
|
||||
------------------------
|
||||
|
||||
.. autoclass:: CQ
|
||||
:members:
|
||||
|
||||
.. autoclass:: Plane
|
||||
:members:
|
||||
|
||||
.. autoclass:: Workplane
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
.. autoclass:: Selector
|
||||
:members:
|
||||
|
||||
.. autoclass:: NearestToPointSelector
|
||||
:members:
|
||||
|
||||
.. autoclass:: ParallelDirSelector
|
||||
:members:
|
||||
|
||||
.. autoclass:: DirectionSelector
|
||||
:members:
|
||||
|
||||
.. autoclass:: PerpendicularDirSelector
|
||||
:members:
|
||||
|
||||
.. autoclass:: TypeSelector
|
||||
:members:
|
||||
|
||||
.. autoclass:: DirectionMinMaxSelector
|
||||
:members:
|
||||
|
||||
.. autoclass:: StringSyntaxSelector
|
||||
:members:
|
276
sphinxdoc/conf.py
Normal file
@ -0,0 +1,276 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# CadQuery documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sat Aug 25 21:10:53 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
|
||||
sys.path.append('../apps')
|
||||
sys.path.append('..')
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
|
||||
|
||||
from django.conf import settings
|
||||
settings._target = None
|
||||
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary','cadfile.cadutils.cq_directive']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'CadQuery'
|
||||
copyright = u'Parametric Products Intellectual Holdings LLC, All Rights Reserved'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#html_theme = 'timlinux-linfiniti-sphinx'
|
||||
html_theme = 'pparts'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {
|
||||
# "headerfont": "'Open Sans',Arial,sans-serif",
|
||||
# #"bodyfont:": "'Open Sans',Arial,sans-serif",
|
||||
# #"headerbg" : "{image: url('/img/bg/body.jpg');color:#000000;}",
|
||||
# "headerbg" : "color:black;",
|
||||
# "footerbg" : "{color:#13171A;}",
|
||||
# "linkcolor": "#84B51E;",
|
||||
## "headercolor1": "#13171A;",
|
||||
# "headercolor2": "#444;",
|
||||
# "headerlinkcolor" : "#13171A;",
|
||||
#}
|
||||
|
||||
#agogo options
|
||||
"""
|
||||
bodyfont (CSS font family): Font for normal text.
|
||||
headerfont (CSS font family): Font for headings.
|
||||
pagewidth (CSS length): Width of the page content, default 70em.
|
||||
documentwidth (CSS length): Width of the document (without sidebar), default 50em.
|
||||
sidebarwidth (CSS length): Width of the sidebar, default 20em.
|
||||
bgcolor (CSS color): Background color.
|
||||
headerbg (CSS value for “background”): background for the header area, default a grayish gradient.
|
||||
footerbg (CSS value for “background”): background for the footer area, default a light gray gradient.
|
||||
linkcolor (CSS color): Body link color.
|
||||
headercolor1, headercolor2 (CSS color): colors for <h1> and <h2> headings.
|
||||
headerlinkcolor (CSS color): Color for the backreference link in headings.
|
||||
textalign (CSS text-align value): Text alignment for the body, default is justify.
|
||||
"""
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
html_title = "Documentation"
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
html_logo = "logo.png"
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
html_show_sourcelink = False
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
html_show_sphinx = False
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'CadQuerydoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'CadQuery.tex', u'CadQuery Documentation',
|
||||
u'David Cowden', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'cadquery', u'CadQuery Documentation',
|
||||
[u'David Cowden'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'CadQuery', u'CadQuery Documentation',
|
||||
u'David Cowden', 'CadQuery', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
688
sphinxdoc/examples.rst
Normal file
@ -0,0 +1,688 @@
|
||||
.. _examples:
|
||||
|
||||
*********************************
|
||||
CadQuery Examples
|
||||
*********************************
|
||||
|
||||
.. automodule:: cadfile.cadutils.cadquery
|
||||
.. automodule:: cadfile.cadutils.cad
|
||||
|
||||
The examples on this page can help you learn how to build objects with CadQuery.
|
||||
|
||||
They are organized from simple to complex, so working through them in order is the best way to absorb them.
|
||||
|
||||
Each example lists the api elements used in the example for easy reference.
|
||||
Items introduced in the example are marked with a **!**
|
||||
|
||||
.. note::
|
||||
|
||||
You may want to work through these examples by pasting the text into a scratchpad on the live website.
|
||||
If you do, make sure to take these steps so that they work:
|
||||
|
||||
1. paste the content into the build() method, properly intented, and
|
||||
2. add the line 'return result' at the end. The samples below are autogenerated, but they use a different
|
||||
syntax than the models on the website need to be.
|
||||
|
||||
.. warning::
|
||||
|
||||
* You have to have an svg capable browser to view these!
|
||||
* For brevity, these examples do not include the MetaData and Header sections required for a
|
||||
fully functional parametric part. See the :ref:`quickstart` for a guide that includes those portions
|
||||
|
||||
.. contents:: List of Examples
|
||||
:backlinks: entry
|
||||
|
||||
|
||||
Simple Rectangular Plate
|
||||
------------------------
|
||||
|
||||
Just about the simplest possible example, a rectangular box
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(2.0,2.0,0.5)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane` **!**
|
||||
* :py:meth:`Workplane.box` **!**
|
||||
|
||||
Plate with Hole
|
||||
------------------------
|
||||
|
||||
A rectangular box, but with a hole added.
|
||||
|
||||
"\>Z" selects the top most face of the resulting box. The hole is located in the center because the default origin
|
||||
of a working plane is at the center of the face. The default hole depth is through the entire part.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(2.0,2.0,0.5).faces(">Z").hole(0.5)
|
||||
|
||||
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.hole` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.box`
|
||||
|
||||
An extruded prismatic solid
|
||||
-------------------------------
|
||||
|
||||
Build a prismatic solid using extrusion. After a drawing operation, the center of the previous object
|
||||
is placed on the stack, and is the reference for the next operation. So in this case, the rect() is drawn
|
||||
centered on the previously draw circle.
|
||||
|
||||
By default, rectangles and circles are centered around the previous working point.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").circle(2.0).rect(0.5,0.75).extrude(0.5)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.circle` **!**
|
||||
* :py:meth:`Workplane.rect` **!**
|
||||
* :py:meth:`Workplane.extrude` **!**
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Building Profiles using lines and arcs
|
||||
--------------------------------------
|
||||
|
||||
Sometimes you need to build complex profiles using lines and arcs. This example builds a prismatic
|
||||
solid from 2-d operations.
|
||||
|
||||
2-d operations maintain a current point, which is initially at the origin. Use close() to finish a
|
||||
closed curve.
|
||||
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").lineTo(2.0,0).lineTo(2.0,1.0).threePointArc((1.0,1.5),(0.0,1.0))\
|
||||
.close().extrude(0.25)
|
||||
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.threePointArc` **!**
|
||||
* :py:meth:`Workplane.lineTo` **!**
|
||||
* :py:meth:`Workplane.extrude`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Moving The Current working point
|
||||
---------------------------------
|
||||
|
||||
In this example, a closed profile is required, with some interior features as well.
|
||||
|
||||
This example also demonstrates using multiple lines of code instead of longer chained commands,
|
||||
though of course in this case it was possible to do it in one long line as well.
|
||||
|
||||
A new work plane center can be established at any point.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").circle(3.0) #current point is the center of the circle, at (0,0)
|
||||
result = result.center(1.5,0.0).rect(0.5,0.5) # new work center is (1.5,0.0)
|
||||
|
||||
result = result.center(-1.5,1.5).circle(0.25) # new work center is ( 0.0,1.5).
|
||||
#the new center is specified relative to the previous center, not global coordinates!
|
||||
|
||||
result = result.extrude(0.25)
|
||||
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.center` **!**
|
||||
* :py:meth:`Workplane`
|
||||
* :py:meth:`Workplane.circle`
|
||||
* :py:meth:`Workplane.rect`
|
||||
* :py:meth:`Workplane.extrude`
|
||||
|
||||
Using Point Lists
|
||||
---------------------------
|
||||
|
||||
Sometimes you need to create a number of features at various locations, and using :py:meth:`Workplane.center`
|
||||
is too cumbersome.
|
||||
|
||||
You can use a list of points to construct multiple objects at once. Most construction methods,
|
||||
like :py:meth:`Workplane.circle` and :py:meth:`Workplane.rect`, will operate on multiple points if they are on the stack
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
r = Workplane("front").circle(2.0) # make base
|
||||
r = r.pushPoints( [ (1.5,0),(0,1.5),(-1.5,0),(0,-1.5) ] ) # now four points are on the stack
|
||||
r = r.circle( 0.25 ) # circle will operate on all four points
|
||||
result = r.extrude(0.125 ) # make prism
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.points` **!**
|
||||
* :py:meth:`Workplane`
|
||||
* :py:meth:`Workplane.circle`
|
||||
* :py:meth:`Workplane.extrude`
|
||||
|
||||
Polygons
|
||||
-------------------------
|
||||
|
||||
You can create polygons for each stack point if you would like. Useful in 3d printers whos firmware does not
|
||||
correct for small hole sizes.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(3.0,4.0,0.25).pushPoints ( [ ( 0,0.75 ),(0,-0.75) ]) \
|
||||
.polygon(6,1.0).cutThruAll()
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.polygon` **!**
|
||||
* :py:meth:`Workplane.pushPoints`
|
||||
* :py:meth:`Workplane.box`
|
||||
|
||||
Polylines
|
||||
-------------------------
|
||||
|
||||
:py:meth:`Workplane.polyline` allows creating a shape from a large number of chained points connected by lines.
|
||||
|
||||
This example uses a polyline to create one half of an i-beam shape, which is mirrored to create the final profile.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
(L,H,W,t) = ( 100.0,20.0,20.0,1.0)
|
||||
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)
|
||||
]
|
||||
result = Workplane("front").polyline(pts).mirrorY().extrude(L)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.polyline` **!**
|
||||
* :py:meth:`Workplane`
|
||||
* :py:meth:`Workplane.mirrorY`
|
||||
* :py:meth:`Workplane.extrude`
|
||||
|
||||
|
||||
|
||||
Defining an Edge with a Spline
|
||||
------------------------------
|
||||
|
||||
This example defines a side using a spline curve through a collection of points. Useful when you have an edge that
|
||||
needs a complex profile
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
s = Workplane("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()
|
||||
result = r.extrude(0.5)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.spline` **!**
|
||||
* :py:meth:`Workplane`
|
||||
* :py:meth:`Workplane.close`
|
||||
* :py:meth:`Workplane.lineTo`
|
||||
* :py:meth:`Workplane.extrude`
|
||||
|
||||
Mirroring Symmetric Geometry
|
||||
-----------------------------
|
||||
|
||||
You can mirror 2-d geometry when your shape is symmetric. In this example we also
|
||||
introduce horizontal and vertical lines, which make for slightly easier coding.
|
||||
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
r = Workplane("front").hLine(1.0) # 1.0 is the distance, not coordinate
|
||||
r = r.vLine(0.5).hLine(-0.25).vLine(-0.25).hLineTo(0.0) # hLineTo allows using xCoordinate not distance
|
||||
result =r.mirrorY().extrude(0.25 ) # mirror the geometry and extrude
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.hLine` **!**
|
||||
* :py:meth:`Workplane.vLine` **!**
|
||||
* :py:meth:`Workplane.hLineTo` **!**
|
||||
* :py:meth:`Workplane.mirrorY` **!**
|
||||
* :py:meth:`Workplane.mirrorX` **!**
|
||||
* :py:meth:`Workplane`
|
||||
* :py:meth:`Workplane.extrude`
|
||||
|
||||
|
||||
Creating Workplanes on Faces
|
||||
-----------------------------
|
||||
|
||||
This example shows how to locate a new workplane on the face of a previously created feature.
|
||||
|
||||
.. note::
|
||||
Using workplanes in this way are a key feature of CadQuery. Unlike typical 3d scripting language,
|
||||
using work planes frees you from tracking the position of various features in variables, and
|
||||
allows the model to adjust itself with removing redundant dimensions
|
||||
|
||||
The :py:meth:`Workplane.faces()` method allows you to select the faces of a resulting solid. It accepts
|
||||
a selector string or object, that allows you to target a single face, and make a workplane oriented on that
|
||||
face.
|
||||
|
||||
Keep in mind that the origin of new workplanes are located at the center of a face by default.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(2,3,0.5) #make a basic prism
|
||||
result = result.faces(">Z").workplane().hole(0.5) #find the top-most face and make a hole
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.faces` **!**
|
||||
* :py:meth:`StringSyntaxSelector` **!**
|
||||
* :ref:`selector_reference` **!**
|
||||
* :py:meth:`Workplane.workplane`
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Locating a Workplane on a vertex
|
||||
---------------------------------
|
||||
|
||||
Normally, the :py:meth:`Workplane.workplane` method requires a face to be selected. But if a vertex is selected
|
||||
**immediately after a face**, :py:meth:`Workplane.workplane` will locate the workplane on the face, with the origin at the vertex instead
|
||||
of at the center of the face
|
||||
|
||||
The example also introduces :py:meth:`Workplane.cutThruAll`, which makes a cut through the entire part, no matter
|
||||
how deep the part is
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(3,2,0.5) #make a basic prism
|
||||
result = result.faces(">Z").vertices("<XY").workplane() #select the lower left vertex and make a workplane
|
||||
result = result.circle(1.0).cutThruAll() #cut the corner out
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.cutThruAll` **!**
|
||||
|
||||
* :ref:`selector_reference` **!**
|
||||
* :py:meth:`Workplane.vertices` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane`
|
||||
* :py:meth:`StringSyntaxSelector` **!**
|
||||
|
||||
Offset Workplanes
|
||||
--------------------------
|
||||
|
||||
Workplanes do not have to lie exactly on a face. When you make a workplane, you can define it at an offset
|
||||
from an existing face.
|
||||
|
||||
This example uses an offset workplane to make a compound object, which is perfectly valid!
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(3,2,0.5) #make a basic prism
|
||||
result = result.faces("<X").workplane(offset=0.75) #workplane is offset from the object surface
|
||||
result = result.circle(1.0).extrude(0.5) #disc
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.extrude`
|
||||
* :ref:`selector_reference` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Rotated Workplanes
|
||||
--------------------------
|
||||
|
||||
You can create a rotated work plane by specifying angles of rotation relative to another workplane
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(4.0,4.0,0.25).faces(">Z").workplane() \
|
||||
.transformed(offset=cad.Vector(0,-1.5,1.0),rotate=cad.Vector(60,0,0)) \
|
||||
.rect(1.5,1.5,forConstruction=True).vertices().hole(0.25)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.transformed` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.rect`
|
||||
* :py:meth:`Workplane.faces`
|
||||
|
||||
Using construction Geometry
|
||||
---------------------------
|
||||
|
||||
You can draw shapes to use the vertices as points to locate other features. Features that are used to
|
||||
locate other features, rather than to create them, are called ``Construction Geometry``
|
||||
|
||||
In the example below, a rectangle is drawn, and its vertices are used to locate a set of holes.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(2,2,0.5).faces(">Z").workplane() \
|
||||
.rect(1.5,1.5,forConstruction=True).vertices().hole(0.125 )
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.rect` (forConstruction=True)
|
||||
* :ref:`selector_reference`
|
||||
* :py:meth:`Workplane.workplane`
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.hole`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Shelling To Create Thin features
|
||||
--------------------------------
|
||||
|
||||
Shelling converts a solid object into a shell of uniform thickness. To shell an object, one or more faces
|
||||
are removed, and then the inside of the solid is 'hollowed out' to make the shell.
|
||||
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(2,2,2).faces("+Z").shell(0.05)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.shell` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.faces`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Making Lofts
|
||||
--------------------------------------------
|
||||
|
||||
A loft is a solid swept through a set of wires. This example creates lofted section between a rectangle
|
||||
and a circular section.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("front").box(4.0,4.0,0.25).faces(">Z").circle(1.5) \
|
||||
.workplane(offset=3.0).rect(0.75,0.5).loft(combine=True)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.loft` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.faces`
|
||||
* :py:meth:`Workplane.circle`
|
||||
* :py:meth:`Workplane.rect`
|
||||
|
||||
Making Counter-bored and counter-sunk holes
|
||||
----------------------------------------------
|
||||
|
||||
Counterbored and countersunk holes are so common that CadQuery creates macros to create them in a single step.
|
||||
|
||||
Similar to :py:meth:`Workplane.hole` , these functions operate on a list of points as well as a single point.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane(Plane.XY()).box(4,2,0.5).faces(">Z").workplane().rect(3.5,1.5,forConstruction=True)\
|
||||
.vertices().cboreHole(0.125, 0.25,0.125,depth=None)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.cboreHole` **!**
|
||||
* :py:meth:`Workplane.cskHole` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.rect`
|
||||
* :py:meth:`Workplane.workplane`
|
||||
* :py:meth:`Workplane.vertices`
|
||||
* :py:meth:`Workplane.faces`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Rounding Corners with Fillet
|
||||
-----------------------------
|
||||
|
||||
Filleting is done by selecting the edges of a solid, and using the fillet function.
|
||||
|
||||
Here we fillet all of the edges of a simple plate.
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
result = Workplane("XY" ).box(3,3,0.5).edges("|Z").fillet(0.125)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.fillet` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.edges`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
Splitting an Object
|
||||
---------------------
|
||||
|
||||
You can split an object using a workplane, and retain either or both halves
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
c = Workplane("XY").box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()
|
||||
|
||||
#now cut it in half sideways
|
||||
result = c.faces(">Y").workplane(-0.5).split(keepTop=True)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.split` **!**
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`Workplane.circle`
|
||||
* :py:meth:`Workplane.cutThruAll`
|
||||
* :py:meth:`Workplane.workplane`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
The Classic OCC Bottle
|
||||
----------------------
|
||||
|
||||
CadQuery is based on the OpenCascade.org (OCC) modeling Kernel. Those who are familiar with OCC know about the
|
||||
famous 'bottle' example. http://www.opencascade.org/org/gettingstarted/appli/
|
||||
|
||||
A pythonOCC version is listed here
|
||||
http://code.google.com/p/pythonocc/source/browse/trunk/src/examples/Tools/InteractiveViewer/scripts/Bottle.py?r=1046
|
||||
|
||||
Of course one difference between this sample and the OCC version is the length. This sample is one of the longer
|
||||
ones at 13 lines, but that's very short compared to the pythonOCC version, which is 10x longer!
|
||||
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
(L,w,t) = (20.0,6.0,3.0)
|
||||
s = Workplane("XY")
|
||||
|
||||
#draw half the profile of the bottle and extrude it
|
||||
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)
|
||||
|
||||
#make a shell
|
||||
result = p.faces(">Z").shell(0.3)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 2
|
||||
|
||||
* :py:meth:`Workplane.extrude`
|
||||
* :py:meth:`Workplane.mirrorX`
|
||||
* :py:meth:`Workplane.threePointArc`
|
||||
* :py:meth:`Workplane.workplane`
|
||||
* :py:meth:`Workplane.vertices`
|
||||
* :py:meth:`Workplane.vLine`
|
||||
* :py:meth:`Workplane.faces`
|
||||
* :py:meth:`Workplane`
|
||||
|
||||
A Parametric Enclosure
|
||||
-----------------------
|
||||
|
||||
.. cq_plot::
|
||||
:height: 400
|
||||
|
||||
#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,True)\
|
||||
.rect((p_outerWidth - 2.0* p_thickness),(p_outerLength - 2.0*p_thickness))\
|
||||
.extrude((p_outerHeight - 2.0*p_thickness),False) #set combine false to produce just the new boss
|
||||
ishell.edges("|Z").fillet(p_sideRadius - p_thickness)
|
||||
|
||||
#make the box outer box
|
||||
box = oshell.cut(ishell)
|
||||
|
||||
#make the screwposts
|
||||
POSTWIDTH = (p_outerWidth - 2.0*p_screwpostInset)
|
||||
POSTLENGTH = (p_outerLength -2.0*p_screwpostInset)
|
||||
|
||||
postCenters = box.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.combineSolids(bottom)
|
||||
|
||||
.. topic:: Api References
|
||||
|
||||
.. hlist::
|
||||
:columns: 3
|
||||
|
||||
* :py:meth:`Workplane.circle`
|
||||
* :py:meth:`Workplane.rect`
|
||||
* :py:meth:`Workplane.extrude`
|
||||
* :py:meth:`Workplane.box`
|
||||
* :py:meth:`CQ.all`
|
||||
* :py:meth:`CQ.faces`
|
||||
* :py:meth:`CQ.vertices`
|
||||
* :py:meth:`CQ.edges`
|
||||
* :py:meth:`CQ.workplane`
|
||||
* :py:meth:`Workplane.fillet`
|
||||
* :py:meth:`Workplane.cut`
|
||||
* :py:meth:`Workplane.combineSolids`
|
||||
* :py:meth:`Workplane.rotateAboutCenter`
|
||||
* :py:meth:`Workplane.cboreHole`
|
||||
* :py:meth:`Workplane.cskHole`
|
||||
* :py:meth:`Workplane.hole`
|
178
sphinxdoc/extending.rst
Normal file
@ -0,0 +1,178 @@
|
||||
.. _extending:
|
||||
|
||||
******************
|
||||
Extending CadQuery
|
||||
******************
|
||||
|
||||
.. automodule:: cadfile.cadutils.cadquery
|
||||
|
||||
If you find that CadQuery doesnt suit your needs, you can easily extend it. CadQuery provides several extension
|
||||
methods:
|
||||
|
||||
* You can load plugins others have developed. This is by far the easiest way to access other code
|
||||
* you can define your own plugins.
|
||||
* you can use FreeCAD script directly
|
||||
|
||||
Loading external Plugins
|
||||
-----------------------
|
||||
|
||||
You can load a plugin using the tools.loadScript(*URL*) directive in your script.
|
||||
|
||||
Using FreeCAD Script
|
||||
--------------------
|
||||
|
||||
The easiest way to extend CadQuery is to simply use FreeCAD script inside of your build method. Just about
|
||||
any valid FreeCAD script will execute just fine. For example, this simple CadQuery script::
|
||||
|
||||
return Workplane("XY").box(1.0,2.0,3.0).val()
|
||||
|
||||
is actually equivalent to::
|
||||
|
||||
return Part.makeBox(1.0,2.0,3.0)
|
||||
|
||||
As long as you return a valid FreeCAD Shape, you can use any FreeCAD methods you like. You can even mix and match the
|
||||
two. For example, consider this script, which creates a FreeCAD box, but then uses cadquery to select its faces::
|
||||
|
||||
box = Part.makeBox(1.0,2.0,3.0)
|
||||
cq = CQ(box).faces(">Z").size() # returns 6
|
||||
|
||||
|
||||
Extending CadQuery: Plugins
|
||||
----------------------------
|
||||
|
||||
Though you can get a lot done with FreeCAD, the code gets pretty nasty in a hurry. CadQuery shields you from
|
||||
a lot of the complexity of the FreeCAD api.
|
||||
|
||||
You can get the best of both worlds by wrapping your freecad script into a CadQuery plugin.
|
||||
|
||||
A CadQuery plugin is simply a function that is attached to the CadQuery :py:meth:`CQ` or :py:meth:`Workplane` class.
|
||||
When connected, your plugin can be used in the chain just like the built-in functions.
|
||||
|
||||
There are a few key concepts important to understand when building a plugin
|
||||
|
||||
|
||||
The Stack
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Every CadQuery object has a local stack, which contains a list of items. The items on the stack will be
|
||||
one of these types:
|
||||
* **A CadQuery SolidReference object**, which holds a reference to a FreeCAD solid
|
||||
* **A FreeCAD object**, a Vertex, Edge, Wire, Face, Shell, Solid, or Compound
|
||||
|
||||
The stack is available by using self.objects, and will always contain at least one object.
|
||||
|
||||
.. note::
|
||||
|
||||
Objects and points on the stack are **always** in global coordinates. Similarly, any objects you
|
||||
create must be created in terms of global coordinates as well!
|
||||
|
||||
|
||||
Preserving the Chain
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
CadQuery's fluent api relies on the ability to chain calls together one after another. For this to work,
|
||||
you must return a valid CadQuery object as a return value. If you choose not to return a CadQuery object,
|
||||
then your plugin will end the chain. Sometimes this is desired for example :py:meth:`CQ.size`
|
||||
|
||||
There are two ways you can safely continue the chain:
|
||||
|
||||
1. **return self** If you simply wish to modify the stack contents, you can simply return a reference to
|
||||
self. This approach is destructive, because the contents of the stack are modified, but it is also the
|
||||
simplest.
|
||||
2. :py:meth:`CQ.newObject` Most of the time, you will want to return a new object. Using newObject will
|
||||
return a new CQ or Workplane object having the stack you specify, and will link this object to the
|
||||
previous one. This preserves the original object and its stack.
|
||||
|
||||
|
||||
Helper Methods
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
When you implement a CadQuery plugin, you are extending CadQuery's base objects. As a result, you can call any
|
||||
CadQuery or Workplane methods from inside of your extension. You can also call a number of internal methods that
|
||||
are designed to aid in plugin creation:
|
||||
|
||||
* :py:meth:`Workplane._pointsOnStack` returns a FreeCAD Vector ( a point ) for each item on the stack. Useful if you
|
||||
are writing a plugin that you'd like to operate on all values on the stack, like :py:meth:`Workplane.circle` and
|
||||
most other built-ins do
|
||||
|
||||
* :py:meth:`Workplane._makeWireAtPoints` will invoke a factory function you supply for all points on the stack,
|
||||
and return a properly constructed cadquery object. This function takes care of registering wires for you
|
||||
and everything like that
|
||||
|
||||
* :py:meth:`Workplane.newObject` returns a new Workplane object with the provided stack, and with its parent set
|
||||
to the current object. The preferred way to continue the chain
|
||||
|
||||
* :py:meth:`Workplane.findSolid` returns the first Solid found in the chain, working from the current object upwards
|
||||
in the chain. commonly used when your plugin will modify an existing solid, or needs to create objects and
|
||||
then combine them onto the 'main' part that is in progress
|
||||
|
||||
* :py:meth:`Workplane._addWire` must be called if you add a wire. This allows the base class to track all the wires
|
||||
that are created, so that they can be managed when extrusion occurs.
|
||||
|
||||
* :py:meth:`Workplane.wire` gathers up all of the edges that have been drawn ( eg, by line, vline, etc ), and
|
||||
attempts to combine them into a single wire, which is returned. This should be used when your plugin creates
|
||||
2-d edges, and you know it is time to collect them into a single wire.
|
||||
|
||||
* :py:meth:`Workplane.plane` provides a reference to the workplane, which allows you to convert between workplane
|
||||
coordinates and global coordinates:
|
||||
* :py:meth:`Plane.toWorldCoords` will convert local coordinates to global ones
|
||||
* :py:meth:`Plane.toLocalCoords` will convet from global coordinates to local coordinates
|
||||
|
||||
Coordinate Systems
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Keep in mind that the user may be using a work plane that has created a local coordinate system. Consequently,
|
||||
the orientation of shapes that you create are often implicitly defined by the user's workplane.
|
||||
|
||||
Any objects that you create must be fully defined in *global coordinates*, even though some or all of the users'
|
||||
inputs may be defined in terms of local coordinates.
|
||||
|
||||
|
||||
Linking in your plugin
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Your plugin is a single method, which is attached to the main Workplane or CadQuery object.
|
||||
|
||||
Your plugin method's first parameter should be 'self', which will provide a reference to base class functionality.
|
||||
You can also accept other arguments.
|
||||
|
||||
To install it, simply attach it to the CadQuery or Workplane object, like this::
|
||||
|
||||
def _yourFunction(self,arg1,arg):
|
||||
do stuff
|
||||
return whatever_you_want
|
||||
|
||||
Workplane.yourPlugin = _yourFunction
|
||||
|
||||
That's it!
|
||||
|
||||
Plugin Example
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
This ultra simple plugin makes cubes of the specified size for each stack point.
|
||||
|
||||
(The cubes are off-center because the boxes have their lower left corner at the reference points.)
|
||||
|
||||
.. cq_plot::
|
||||
|
||||
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 into cadQuery
|
||||
Workplane.makeCubes = makeCubes
|
||||
|
||||
#use the plugin
|
||||
result = Workplane("XY").box(6.0,8.0,0.5).faces(">Z").rect(4.0,4.0,forConstruction=True).vertices() \
|
||||
.makeCubes(1.0).combineSolids()
|
||||
|
||||
|
202
sphinxdoc/fileformat.rst
Normal file
@ -0,0 +1,202 @@
|
||||
.. _cadquery_reference:
|
||||
|
||||
********************************
|
||||
ModelScript Format Reference
|
||||
********************************
|
||||
|
||||
ParametricParts ModelScripts define a parametric 3D model that can be executed and customized by an end user.
|
||||
CadQuery scripts are pure python scripts that follow a standard format. Each script contains these main components:
|
||||
|
||||
:MetaData:
|
||||
*(Mandatory)* Defines the attributes that describe the model, such as version and unit of measure
|
||||
|
||||
:Parameters:
|
||||
*(Optional)* Defines parameters and their default values, which can be
|
||||
manipulated by users to customize the object. Parameters are defined by creating local variables
|
||||
of a particular class type. Presets and groups organize parameters to make them easier to use
|
||||
|
||||
:build script:
|
||||
*(Mandatory)* Constructs the model once parameter values are collected and the model is validated.
|
||||
The script must return a solid object, or a cadquery solid
|
||||
|
||||
The Script Life-cycle
|
||||
----------------------
|
||||
|
||||
CadQuery scripts have the following lifecycle when they are executed by a user via the web interface:
|
||||
|
||||
1. **Load Script** If it is valid, the parameters and MetaData
|
||||
are loaded. A number of special objects are automatically available to your script
|
||||
|
||||
2. **Display Model to User** The parameters and default values are displayed to the user.
|
||||
The model is rendered and displayed to the user using the default values
|
||||
|
||||
3. **User selects new parameter values** , either by selecting
|
||||
preset combinations, or by providing values for each parameter
|
||||
|
||||
4. **Build the model** If validation is successful, the model is re-built, and the preview window is updated
|
||||
|
||||
5. **User downloads** If the user chooses to download the model as STL, STEP, or AMF, the model is re-built
|
||||
again for download.
|
||||
|
||||
|
||||
A Full Example Script
|
||||
----------------------
|
||||
|
||||
This script demonstrates all of the model elements available. Each is briefly introduced in the sample text,
|
||||
and then described in more detail after the sample::
|
||||
|
||||
"""
|
||||
Comments and Copyright Statement
|
||||
"""
|
||||
|
||||
#
|
||||
# metadata describes your model
|
||||
#
|
||||
UOM = "mm"
|
||||
VERSION = 1.0
|
||||
|
||||
#
|
||||
# parameter definitions. Valid parameter types are FloatParam,IntParam,and BooleanParam
|
||||
# each paraemter can have min and max values, a description, and a set of named preset values
|
||||
#
|
||||
p_diam = FloatParam(min=1.0,max=500.0,presets={'default':40.0,'small':2.0,'big':200.0},group="Basics", desc="Diameter");
|
||||
|
||||
#
|
||||
# build the model based on user selected parameter values.
|
||||
# Must return a FreeCAD solid before exiting.
|
||||
#
|
||||
def build():
|
||||
return Part.makeSphere(p_diam.value);
|
||||
|
||||
|
||||
Each section of the script is described in more detail below
|
||||
|
||||
Metadata
|
||||
----------------
|
||||
|
||||
Model metadata is provided by setting a dictionary variable called METADATA in the script. You can provide
|
||||
any metadata you choose, but only these values are currently used:
|
||||
|
||||
:UOM:
|
||||
The unit of measure of your model. in and mm are common values, but others are allowed.
|
||||
Some model formats like AMF can accept units of measure, which streamlines the printing process. **[OPTIONAL]**
|
||||
|
||||
:VERSION:
|
||||
The script format version. Valid versions are established by ParametricParts, currently only version 1.0 is
|
||||
valid. If omitted, the latest version is assumed. **[OPTIONAL]**
|
||||
|
||||
|
||||
Other metadata fields may be added in the future.
|
||||
|
||||
Parameters
|
||||
----------------
|
||||
|
||||
Model parameters provide the flexibility users need to customize your model. Parameters are optional, but most
|
||||
users will expect at least a couple of parameters for your model to qualify as 'parametric'.
|
||||
|
||||
|
||||
Parameters can be named whatever you would like. By convention, it is common to name them *p_<name>*, indicating
|
||||
"parameter".
|
||||
|
||||
|
||||
Each parameter has a particular type ( Float, Integer, Boolean ). Parameters also have optional attributes, which are
|
||||
provided as keyword arguments:
|
||||
|
||||
:desc:
|
||||
A description of the parameter, displayed to the user if help is needed [Optional]
|
||||
|
||||
:min:
|
||||
The minimum value ( not applicable to Boolean ) [Optional]
|
||||
|
||||
:max:
|
||||
The maximum value ( not applicable to Boolean ) [Optional]
|
||||
|
||||
:presets:
|
||||
A dictionary containing key-value pairs. Each key is the name of a preset, and the value is the value the
|
||||
parameter will take when the preset is selected by the user.
|
||||
|
||||
|
||||
When a model defines presets, the user is presented with a choice of available presets in a drop-down-list.
|
||||
Selecting a preset changes the values of all parameters to their associated values.
|
||||
|
||||
If it exists, the special preset named 'default' will be used to populate the default values when the user
|
||||
is initially presented with the model.
|
||||
|
||||
When the model is built, the parameters are checked to ensure they meet the constraints. If they do not,
|
||||
an error occurs.
|
||||
|
||||
:group:
|
||||
If provided, parameters will be grouped together when displayed to the user. Any ungrouped parameters
|
||||
will display in a special group named `default`. Groups help divide a long list of parameters to make
|
||||
them easier to understand. Examples might include 'basics' and 'advanced'
|
||||
|
||||
|
||||
Build Method
|
||||
-----------------------
|
||||
|
||||
The heart of your model is the build method. Your build method must be called 'build'::
|
||||
|
||||
def build():
|
||||
return Workplane("XY").box(1,1,1)
|
||||
|
||||
Your build method use any combination of FreeCAD, python, and CadQuery to construct objects.
|
||||
You must return one of two things:
|
||||
|
||||
1. A CadQuery object, or
|
||||
2. A FreeCAD object
|
||||
|
||||
In your build script,you retrieve the values of the parameters by using ``<parameter_name>.value``.
|
||||
|
||||
The following modules are available when your script runs:
|
||||
|
||||
Scripts Using CadQuery Syntax
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
:python syntax:
|
||||
Python loops, dictionaries, lists, and other standard language structures are available.
|
||||
|
||||
:math:
|
||||
Python's math package is imported for you to use
|
||||
|
||||
:FloatParam,IntegerParam,BooleanParam:
|
||||
Parameter types used to declare parameters
|
||||
|
||||
:Workplane:
|
||||
The CadQuery workplane object, which is the typical starting point for most scripts
|
||||
|
||||
:CQ:
|
||||
The CadQuery object, in case you need to decorate a normal FreeCAD object
|
||||
|
||||
:Plane:
|
||||
The CadQuery Plane object, in case you need to create non-standard planes
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
Though your script is a standard python script, it does **not** run in a standard python environment.
|
||||
|
||||
For security reasons, most python packages, like sys, os, import, and urllib are restricted.
|
||||
|
||||
|
||||
FreeCAD Build Scripts
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
It is recommended that you use CadQuery for your model scripts-- the syntax is much shorter and more convienient.
|
||||
|
||||
But if you are willing to write more code, you can get access to all of the features that the FreeCAD library provides.
|
||||
|
||||
When your script executes, these FreeCAD objects are in scope as well:
|
||||
|
||||
:Part:
|
||||
FreeCAD.Part
|
||||
:Vector:
|
||||
FreeCAD.Base.Vector
|
||||
:Base:
|
||||
FreeCAD.Base
|
||||
|
||||
**If you use a FreeCAD build script, your build method must return a FreeCAD shape object.**
|
||||
|
||||
Should you choose to write your model with the lower-level FreeCAD scripts, you may find this documentation useful:
|
||||
|
||||
http://sourceforge.net/apps/mediawiki/free-cad/index.php?title=FreeCAD_API
|
||||
|
43
sphinxdoc/index.rst
Normal file
@ -0,0 +1,43 @@
|
||||
.. CadQuery documentation master file, created by
|
||||
sphinx-quickstart on Sat Aug 25 21:10:53 2012.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Parametric Parts Documentation Home
|
||||
===================================
|
||||
|
||||
Parametric Parts is a technology platform that offers:
|
||||
|
||||
* **For Users**: Customize, download, and print models easily using only a web browser. Every model is parametric.
|
||||
* **For Designers**: Create models with an easy to use, fluent API called CadQuery. ( See :ref:`examples` )
|
||||
* **For Developers**: :ref:`buildservice` allows other technology platforms to build models while controlling the user
|
||||
experience
|
||||
|
||||
|
||||
Contents
|
||||
==================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
intro.rst
|
||||
quickstart.rst
|
||||
fileformat.rst
|
||||
cadquerybasics.rst
|
||||
examples.rst
|
||||
apireference.rst
|
||||
primitiveref.rst
|
||||
selectors.rst
|
||||
classreference.rst
|
||||
|
||||
restservice.rst
|
||||
|
||||
roadmap.rst
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
70
sphinxdoc/intro.rst
Normal file
@ -0,0 +1,70 @@
|
||||
.. _what_is_cadquery:
|
||||
|
||||
*********************
|
||||
Introduction
|
||||
*********************
|
||||
|
||||
What is a ParametricParts Model Script?
|
||||
========================================
|
||||
|
||||
A Model Script is a python script that builds a 3d model in response to user inputs.
|
||||
|
||||
Model Scripts are written in python. They can use two different APIs:
|
||||
1. FreeCAD Scripts, or
|
||||
2. a new, fluent-api called CadQuery.
|
||||
|
||||
CadQuery is an intuitive, easy-to-use language for building parametric 3D CAD models. It has several goals:
|
||||
|
||||
* Build models with scripts that are as close as possible to how you'd describe the object to a human.
|
||||
|
||||
* Create parametric models that can be very easily customized by end users
|
||||
|
||||
* Output high quality CAD formats like STEP and AMF in addition to traditional STL
|
||||
|
||||
* Provide a non-proprietary, plain text model format that can be edited and executed with only a web browser
|
||||
|
||||
|
||||
CadQuery is a Python module that provides a high-level wrapper around the
|
||||
(`FreeCAD <http://sourceforge.net/apps/mediawiki/free-cad/index.php?title=Main_Page>`_) python libraries.
|
||||
|
||||
Where does the name CadQuery come from?
|
||||
===============================
|
||||
|
||||
CadQuery is inspired by ( `jQuery <http://www.jquery.com>`_ ), a popular framework that
|
||||
revolutionized web development involving javascript.
|
||||
|
||||
CadQuery is for 3D CAD what jQuery is for javascript.
|
||||
If you are familiar with how jQuery, you will probably recognize several jQuery features that CadQuery uses:
|
||||
|
||||
* A fluent api to create clean, easy to read code
|
||||
|
||||
* Ability to use the library along side other python libraries
|
||||
|
||||
* Clear and complete documentation, with plenty of samples.
|
||||
|
||||
|
||||
Why ParametricParts instead of OpenSCAD?
|
||||
==================================
|
||||
|
||||
CadQuery is based on FreeCAD,which is in turn based on the OpenCascade modelling kernel. CadQuery/FreeCAD scripts
|
||||
share many features with OpenSCAD, another open source, script based, parametric model generator.
|
||||
|
||||
The primary advantage of OpenSCAD is the large number of already existing model libaries that exist already. So why not simply use OpenSCAD?
|
||||
|
||||
CadQuery scripts run from ParametricParts.com have several key advantages over OpenSCAD ( including the various web-based SCAD solutions):
|
||||
|
||||
1. **The scripts use a standard programming language**, python, and thus can benefit from the associated infrastructure.
|
||||
This includes many standard libraries and IDEs
|
||||
|
||||
2. **More powerful CAD kernel** OpenCascade is much more powerful than CGAL. Features supported natively
|
||||
by OCC include NURBS, splines, surface sewing, STL repair, STEP import/export, and other complex operations,
|
||||
in addition to the standard CSG operations supported by CGAL
|
||||
|
||||
3. **Ability to import/export STEP** We think the ability to begin with a STEP model, created in a CAD package,
|
||||
and then add parametric features is key. This is possible in OpenSCAD using STL, but STL is a lossy format
|
||||
|
||||
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.
|
||||
|
190
sphinxdoc/make.bat
Normal file
@ -0,0 +1,190 @@
|
||||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\CadQuery.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\CadQuery.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
9
sphinxdoc/primer.rst
Normal file
@ -0,0 +1,9 @@
|
||||
.. _3d_cad_primer:
|
||||
|
||||
***********************
|
||||
3D CAD Primer
|
||||
***********************
|
||||
|
||||
This section provides a basic introduction to 3D modeling. It will get you started with the basics. After that,
|
||||
you may want to do some heavier reading on the subject (PUT LINKS HERE )
|
||||
|
57
sphinxdoc/primitiveref.rst
Normal file
@ -0,0 +1,57 @@
|
||||
.. _primreference:
|
||||
|
||||
***********************
|
||||
Primitive Class Reference
|
||||
***********************
|
||||
|
||||
.. automodule:: cadfile.cadutils.cad
|
||||
|
||||
|
||||
.. autosummary::
|
||||
|
||||
Plane
|
||||
Vector
|
||||
Solid
|
||||
Shell
|
||||
Wire
|
||||
Edge
|
||||
Vertex
|
||||
|
||||
Geometry Classes
|
||||
------------------
|
||||
|
||||
.. autoclass:: Vector
|
||||
:members:
|
||||
|
||||
.. autoclass:: Plane
|
||||
:members:
|
||||
|
||||
Shape Base Class
|
||||
-------------------
|
||||
|
||||
All objects inherit from Shape, which as basic manipulation methods:
|
||||
|
||||
.. autoclass:: Shape
|
||||
:members:
|
||||
|
||||
Primitive Classes
|
||||
--------------------
|
||||
|
||||
.. autoclass:: Solid
|
||||
:members:
|
||||
|
||||
|
||||
.. autoclass:: Shell
|
||||
:members:
|
||||
|
||||
|
||||
.. autoclass:: Wire
|
||||
:members:
|
||||
|
||||
|
||||
.. autoclass:: Edge
|
||||
:members:
|
||||
|
||||
|
||||
.. autoclass:: Vertex
|
||||
:members:
|
305
sphinxdoc/quickstart.rst
Normal file
@ -0,0 +1,305 @@
|
||||
|
||||
.. module:: cadfile.cadutils.cadquery
|
||||
|
||||
.. _quickstart:
|
||||
|
||||
***********************
|
||||
ModelScript QuickStart
|
||||
***********************
|
||||
|
||||
Want a quick glimpse of Parametric Parts ModelScripts? You're at the right place!
|
||||
This quickstart will demonstrate the basics of ModelScripts using a simple example
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
**WebGL Capable Browser**
|
||||
|
||||
CadQuery renders models in your browser using WebGL-- which is supported by most browsers *except for IE*
|
||||
You can follow along without IE, but you will not be able to see the model dynamically rendered
|
||||
|
||||
|
||||
What we'll accomplish
|
||||
=====================
|
||||
|
||||
Our finished object will look like this:
|
||||
|
||||
.. image:: quickstart.png
|
||||
|
||||
|
||||
**We would like our block to have these features:**
|
||||
|
||||
1. It should be sized to hold a single 608 ( 'skate' ) bearing, in the center of the block.
|
||||
2. It should have counter sunk holes for M2 socket head cap screws at the corners
|
||||
3. The length and width of the block should be configurable by the user to any reasonable size.
|
||||
|
||||
A human would describe this as:
|
||||
|
||||
"A rectangular block 80mm x 60mm x 30mm , with countersunk holes for M2 socket head cap screws
|
||||
at the corners, and a circular pocket 22mm in diameter in the middle for a bearing"
|
||||
|
||||
Human descriptions are very elegant, right?
|
||||
Hopefully our finished script will not be too much more complex than this human-oriented description.
|
||||
|
||||
Let's see how we do.
|
||||
|
||||
Start a new Model
|
||||
==================================
|
||||
|
||||
CadQuery comes with an online, interactive default model as a starting point. Lets open up that tool
|
||||
`here <http://www.parametricparts.com/parts/create>`_
|
||||
|
||||
You should see the dynamic model creator page, which will display a sample model:
|
||||
|
||||
.. image:: quickstart-1.png
|
||||
|
||||
Take a minute to play with this model. Here are a few things to try:
|
||||
|
||||
1. Use the mouse to rotate the block
|
||||
2. Play with the view controls under the image
|
||||
3. change the length ( the only available parameter),
|
||||
and use the preview button to re-display the updated model
|
||||
4. Change the preset value to `short`
|
||||
5. Edit the model script itself. Change the hard-coded width and thickness values and click 'update script'
|
||||
to re-display the model.
|
||||
|
||||
At this point, you should have some idea how to interact with the sample model, so lets get to work on the project.
|
||||
|
||||
Modify MetaData and Parameters
|
||||
==============================
|
||||
|
||||
Each model has metadata that describes the model's properties. The default Unit of Measure (UOM) will work:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 1
|
||||
|
||||
UOM = "mm"
|
||||
|
||||
|
||||
Next, lets set up the parameters. Parameters are `placeholders` that users can modify separately from the script itself.
|
||||
The default model has a single parameter, ``length``. Lets add a ``height`` parameter too
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 4
|
||||
|
||||
UOM = "mm"
|
||||
|
||||
length = FloatParam(min=30.0,max=200.0,presets={'default':80.0,'short':30.0},desc="Length of the block")
|
||||
height = FloatParam(min=30.0,max=200.0,presets={'default':60.0,'short':30.0},desc="Height of the block")
|
||||
thickness = 10.0
|
||||
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness)
|
||||
|
||||
We've set the minimum values to 30 mm, since that's about as small as it could be while having room for a bearing 22mm
|
||||
in diameter. We've also set the default values to be those we'd like to start with: 80mm for the length and 60mm for the
|
||||
height.
|
||||
|
||||
Now, modify the build script to use your width value to make the block by changing ``height`` to
|
||||
``height.value``
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 3
|
||||
|
||||
...
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness)
|
||||
|
||||
The value property always returns the ``user-adjusted`` value of the parameter. That's good enough for now.
|
||||
Click "Save Changes" and you should see your 80x60x10mm base plate, like this:
|
||||
|
||||
.. image:: quickstart-2.png
|
||||
|
||||
If you'd like to come back to this model later, the url bar links to the newly created part.
|
||||
|
||||
Now lets move on and make this boring plate into a pillow block.
|
||||
|
||||
|
||||
Add the Holes
|
||||
================
|
||||
|
||||
Our pillow block needs to have a 22mm diameter hole in the center of this block to hold the bearing.
|
||||
|
||||
This modification will do the trick:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 3
|
||||
|
||||
...
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness).faces(">Z").workplane().hole(22.0)
|
||||
|
||||
Rebuild your model by clicking "Save Model" at the bottom. Your block should look like this:
|
||||
|
||||
.. image:: quickstart-3.png
|
||||
|
||||
|
||||
The code is pretty compact, and works like this:
|
||||
* :py:meth:`Workplane.faces` selects the top-most face in the Z direction, and
|
||||
* :py:meth:`Workplane.workplane` begins a new workplane located on this face
|
||||
* :py:meth:`Workplane.hole` drills a hole through the part 22mm in diamter
|
||||
|
||||
.. note::
|
||||
|
||||
Don't worry about the CadQuery syntax now.. you can learn all about it in the :ref:`apireference` later.
|
||||
|
||||
More Holes
|
||||
============
|
||||
|
||||
Ok, that hole was not too hard, but what about the counter-bored holes in the corners?
|
||||
|
||||
An M2 Socket head cap screw has these dimensions:
|
||||
|
||||
* **Head Diameter** : 3.8 mm
|
||||
* **Head height** : 2.0 mm
|
||||
* **Clearance Hole** : 2.4 mm
|
||||
* **CounterBore diameter** : 4.4 mm
|
||||
|
||||
The centers of these holes should be 4mm from the edges of the block. And,
|
||||
we want the block to work correctly even when the block is re-sized by the user.
|
||||
|
||||
**Don't tell me** we'll have to repeat the steps above 8 times to get counter-bored holes?
|
||||
|
||||
Good news!-- we can get the job done with just two lines of code. Here's the code we need:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 4-5
|
||||
|
||||
...
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness).faces(">Z").workplane().hole(22.0) \
|
||||
.faces(">Z").workplane() \
|
||||
.rect(length.value-8.0,height.value-8.0,forConstruction=True) \
|
||||
.vertices().cboreHole(2.4,4.4,2.1)
|
||||
|
||||
You should see something like this:
|
||||
|
||||
.. image:: quickstart-4.png
|
||||
|
||||
Lets Break that down a bit
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
**Line 4** selects the top-most face of the block, and creates a workplane on the top that face, which we'll use to
|
||||
define the centers of the holes in the corners:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 4
|
||||
|
||||
...
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness).faces(">Z").workplane().hole(22.0) \
|
||||
.faces(">Z").workplane() \
|
||||
.rect(length.value-8.0,width.value-8.0,forConstruction=True) \
|
||||
.vertices().cboreHole(2.4,4.4,2.1)
|
||||
|
||||
|
||||
**Line 5** draws a rectangle 8mm smaller than the overall length and width of the block,which we will use to
|
||||
locate the corner holes:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 5
|
||||
|
||||
...
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness).faces(">Z").workplane().hole(22.0) \
|
||||
.faces(">Z").workplane() \
|
||||
.rect(length.value-8.0,width.value-8.0,forConstruction=True) \
|
||||
.vertices().cboreHole(2.4,4.4,2.1)
|
||||
|
||||
There are a couple of things to note about this line:
|
||||
|
||||
1. The :py:meth:`Workplane.rect` function draws a rectangle. **forConstruction=True**
|
||||
tells CadQuery that this rectangle will not form a part of the solid,
|
||||
but we are just using it to help define some other geometry.
|
||||
2. The center point of a workplane on a face is always at the center of the face, which works well here
|
||||
3. Unless you specifiy otherwise, a rectangle is drawn with its center on the current workplane center-- in
|
||||
this case, the center of the top face of the block. So this rectangle will be centered on the face
|
||||
|
||||
|
||||
**Line 6** selects the corners of the rectangle, and makes the holes:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 6
|
||||
|
||||
...
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness).faces(">Z").workplane().hole(22.0) \
|
||||
.faces(">Z").workplane() \
|
||||
.rect(length.value-8.0,width.value-8.0,forConstruction=True) \
|
||||
.vertices().cboreHole(2.4,4.4,2.1)
|
||||
|
||||
Notes about this line:
|
||||
|
||||
1. The :py:meth:`CQ.vertices` function selects the corners of the rectangle
|
||||
2. The :py:meth:`Workplane.cboreHole` function is a handy CadQuery function that makes a counterbored hole
|
||||
3. ``cboreHole``, like most other CadQuery functions, operate on the values on the stack. In this case, since
|
||||
selected the four vertices before calling the function, the function operates on each of the four points--
|
||||
which results in a counterbore hole at the corners.
|
||||
|
||||
Presets
|
||||
===========
|
||||
|
||||
Almost done. This model is pretty easy to configure, but we can make it even easier by providing users with a few
|
||||
'out of the box' options to choose from. Lets provide two preset options:
|
||||
|
||||
* **Small** : 30 mm x 40mm
|
||||
* **Square-Medium** : 50 mm x 50mm
|
||||
|
||||
We can do that using the preset dictionaries in the parameter definition:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
:emphasize-lines: 2-3
|
||||
|
||||
...
|
||||
length = FloatParam(min=10.0,max=500.0,presets={'default':100.0,'small':30.0,'square-medium':50},desc="Length of the box")
|
||||
height = FloatParam(min=30.0,max=200.0,presets={'default':60.0,'small':40.0,'square-medium':50},desc="Height of the block")
|
||||
|
||||
Now save the model and have a look at the preset DDLB-- you'll see that you can easily switch between these
|
||||
configurations:
|
||||
|
||||
.. image:: quickstart-5.png
|
||||
|
||||
|
||||
Done!
|
||||
============
|
||||
|
||||
And... We're done! Congratulations, you just made a parametric, 3d model with 15 lines of code.Users can use this
|
||||
model to generate pillow blocks in any size they would like
|
||||
|
||||
For completeness, Here's a copy of the finished model:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
UOM = "mm"
|
||||
|
||||
length = FloatParam(min=10.0,max=500.0,presets={'default':100.0,'small':30.0,'square-medium':50},desc="Length of the box")
|
||||
height = FloatParam(min=30.0,max=200.0,presets={'default':60.0,'small':40.0,'square-medium':50},desc="Height of the block")
|
||||
|
||||
width = 40.0
|
||||
thickness = 10.0
|
||||
|
||||
def build():
|
||||
return Workplane("XY").box(length.value,height.value,thickness).faces(">Z").workplane().hole(22.0) \
|
||||
.faces(">Z").workplane() \
|
||||
.rect(length.value-8.0,height.value-8.0,forConstruction=True) \
|
||||
.vertices().cboreHole(2.4,4.4,2.1)
|
||||
|
||||
|
||||
Want to learn more?
|
||||
====================
|
||||
|
||||
* The :ref:`examples` contains lots of examples demonstrating cadquery features
|
||||
* The :ref:`cadquery_reference` describes the file format in detail
|
||||
* The :ref:`apireference` is a good overview of language features grouped by function
|
||||
* The :ref:`classreference` is the hard-core listing of all functions available.
|
114
sphinxdoc/restservice.rst
Normal file
@ -0,0 +1,114 @@
|
||||
.. _buildservice:
|
||||
|
||||
******************************************
|
||||
The Parametric Parts Build Service
|
||||
******************************************
|
||||
|
||||
|
||||
If you have registered for an account, you can use the REST api to build models from your website or platform.
|
||||
Each request to the service will construct a model in the format you choose.
|
||||
|
||||
|
||||
Using the Build Service
|
||||
-------------------------
|
||||
|
||||
The Build Service endpoint is `<https://parametricparts.com/parts/build>`_
|
||||
|
||||
In each request, you provide four main things via either a GET or a POST :
|
||||
|
||||
1. **An API Key**, to identify yourself.
|
||||
2. **A ModelScript to build**, either by providing the entire script, or the id of a model stored on
|
||||
parametricparts.com,
|
||||
3. **The type of output** you want,
|
||||
4. **The Model parameters** that should be supplied to the model.
|
||||
|
||||
.. note::
|
||||
|
||||
GET or POSTs are allowed, but be aware that URLs for GET requests are limited to 4K,
|
||||
so POSTs are advised if you are sending your modelScript via the URL
|
||||
|
||||
The output streamed in the format you have requested.
|
||||
|
||||
Errors are provided using standard HTTP error codes:
|
||||
|
||||
:200: if the build is a success
|
||||
:403: if the APIKey is invalid, or if your account cannot execute any more downloads
|
||||
:404: if the requested model cannot be found
|
||||
:50X: if there is a problem generating the model
|
||||
|
||||
Build Service Parameters
|
||||
--------------------------
|
||||
|
||||
All parameters must be URL encoded:
|
||||
|
||||
:key:
|
||||
(Required) Your API Key. See :ref:`gettingakey` If you do not have one.
|
||||
|
||||
:id:
|
||||
(Either id or s is Required) The id of the ParametricParts.com ModelScript to build. The id is the last part of the url
|
||||
when viewing the model: http://parametricparts.com/parts/<modelId>. Model ids are between 7 and 9
|
||||
characters, for example '4hskpb69'.
|
||||
|
||||
:s:
|
||||
(Either id or s is Required) The ModelScript to build. This should be a valid parametricparts.com ModelScript.
|
||||
If both id and s are provided, s takes precedence.
|
||||
|
||||
:type:
|
||||
(Required) ("STL" | "STEP" | "AMF" | "TJS" ). The type of output you want to receive. STL, STEP,
|
||||
and AMF return the corresponding industry standard format.
|
||||
TJS will return JSON content suitable for display in a Three.js scene.
|
||||
|
||||
:preset:
|
||||
(Optional) The name of a preset defined in the ModelScript. If omitted, other parameters are used.
|
||||
If a preset is provided in addition to parameters, then the preset is applied first, and then
|
||||
parameters are set afterwards.
|
||||
|
||||
:<params>:
|
||||
(Optional) Remaining URL parameters are mapped onto ModelScript parameters of the same name. Each
|
||||
parameter value must have the datatype corresponding to the parameter in the ModelScript. To supply multiple
|
||||
parameters, send an HTTP parameter for each desired value, having name matching the name of the ModelScript
|
||||
parameter, and value having the value for that parameter. If no
|
||||
parameters are provided, output is generated using ModelScript defaults.
|
||||
|
||||
Example
|
||||
--------------------------
|
||||
|
||||
This example builds STEP for a trivial model, without supplying any model parameters or presets::
|
||||
|
||||
POST https://parametricparts.com/parts/build HTTP/1.1
|
||||
key:259cd575c9a2998420ac65f21b2d6b2a
|
||||
s:def+build%28%29%3A%0D%0A++++return+Part.makeBox%281%2C2%2C3%29%0D%0A++++++++
|
||||
type:AMF
|
||||
|
||||
|
||||
This example selects an existing model (2qus7a32 ) on the server, and requests
|
||||
preset 'short', as well as adjusting parameter 'p_length' to value 120::
|
||||
|
||||
POST https://parametricparts.com/parts/build HTTP/1.1
|
||||
key:259cd575c9a2998420ac65f21b2d6b2a
|
||||
id:2qus7a32
|
||||
type:STL
|
||||
preset:short
|
||||
p_length:120
|
||||
|
||||
|
||||
.. _gettingakey:
|
||||
|
||||
Signing Up
|
||||
-----------------------
|
||||
|
||||
In order to use the API, you first need to have an API key. To get one:
|
||||
|
||||
1. `Sign Up <https://parametricparts.com/account/signup>`_ for a ParametricParts account
|
||||
2. `Contact ParametricParts Support <http://support.parametricparts.com/customer/portal/emails/new>`_ to request API key access.
|
||||
API keys usually require an enterprise license, but are available for free evaluation if you request access
|
||||
3. Log onto your ParametricParts account, and generate an API Key using the `API Keys <https://localhost:8080/key/keys>`_ link.
|
||||
4. Test your api key using the api key tester `Here <https://parametricparts.com/apitester>`_
|
||||
If the test goes well, you'll see STL output from the sample script.
|
||||
|
||||
Now you are ready to make REST requests to build models.
|
||||
|
||||
.. warning::
|
||||
|
||||
Make sure to keep your API Key secret, as any requests that use your key will be charged to your account.
|
||||
You can disable or generate a new API Key from your account page.
|
172
sphinxdoc/roadmap.rst
Normal file
@ -0,0 +1,172 @@
|
||||
.. _roadmap:
|
||||
|
||||
**************************
|
||||
RoadMap: Planned Features
|
||||
**************************
|
||||
|
||||
**CadQuery is not even close to finished!!!**
|
||||
|
||||
Many features are planned for later versions. This page tracks them. If you find that you need features
|
||||
not listed here, let us know!
|
||||
|
||||
Core
|
||||
--------------------
|
||||
|
||||
end(n)
|
||||
allows moving backwards a fixed number of parents in the chain, eg end(3) is same as end().end().end()
|
||||
|
||||
FreeCAD object wrappers
|
||||
return CQ wrappers for FreeCAD shapes instead of the native FreeCAD objects.
|
||||
|
||||
Improved iteration tools for plugin developers
|
||||
make it easier to iterate over points and wires for plugins
|
||||
|
||||
More parameter types (String? )
|
||||
|
||||
face.outerWire
|
||||
allow selecting the outerWire of a face, so that it can be used for reference geometry or offsets
|
||||
|
||||
Selectors
|
||||
--------------------
|
||||
|
||||
Chained Selectors
|
||||
Space delimited selectors should be unioned to allow multiple selections. For example ">Z >X"
|
||||
|
||||
Ad-hoc axes
|
||||
for example, >(1,2,1) would select a face with normal in the 1,2,1 direction
|
||||
|
||||
logic inversion
|
||||
! or not to invert logic, such as "!(>Z)" to select faces _other_ than the most z facing
|
||||
|
||||
closest to point
|
||||
support faces, points, or edges closest to a provided point
|
||||
|
||||
tagged entities
|
||||
support tagging entities when they are created, so they can be selected later on using that tag.
|
||||
ideally, tags are propagated to features that are created from these features ( ie, an edge tagged with 'foo'
|
||||
that is later extruded into a face means that face would be tagged with 'foo' as well )
|
||||
|
||||
|
||||
Workplanes
|
||||
--------------------
|
||||
|
||||
rotated workplanes
|
||||
support creation of workplanes at an angle to another plane or face
|
||||
|
||||
workplane local rotations
|
||||
rotate the coordinate system of a workplane by an angle.
|
||||
|
||||
make a workplane from a wire
|
||||
useful to select outer wire and then operate from there, to allow offsets
|
||||
|
||||
2-d operations
|
||||
-------------------
|
||||
|
||||
offsets
|
||||
offset profiles, including circles, rects, and other profiles.
|
||||
|
||||
ellipses
|
||||
create elipses and portions of elipses
|
||||
|
||||
regular polygons
|
||||
several construction methods:
|
||||
* number of sides and side length
|
||||
* number of sides inscribed in circle
|
||||
* number of sides circumscribed by circle
|
||||
|
||||
arc construction using relative measures
|
||||
instead of forcing use of absolute workplane coordinates
|
||||
|
||||
tangent arcs
|
||||
after a line
|
||||
|
||||
centerpoint arcs
|
||||
including portions of arcs as well as with end points specified
|
||||
|
||||
trimming
|
||||
ability to use construction geometry to trim other entities
|
||||
|
||||
construction lines
|
||||
especially centerlines
|
||||
|
||||
2-d fillets
|
||||
for a rectangle, or for consecutive selected lines
|
||||
|
||||
2-d chamfers
|
||||
based on rectangles, polygons, polylines, or adjacent selected lines
|
||||
|
||||
mirror around centerline
|
||||
using centerline construction geometry
|
||||
|
||||
rectangular array
|
||||
automate creation of equally spread points
|
||||
|
||||
polar array
|
||||
create equally spaced copies of a feature around a circle
|
||||
perhaps based on a construction circle?
|
||||
|
||||
midpoint selection
|
||||
select midpoints of lines, arcs
|
||||
|
||||
face center
|
||||
explicit selection of face center
|
||||
|
||||
manipulate spline control points
|
||||
so that the shape of a spline can be more accurately controlled
|
||||
|
||||
feature snap
|
||||
project geometry in the rest of the part into the work plane, so that
|
||||
they can be selected and used as references for other features.
|
||||
|
||||
polyline edges
|
||||
allow polyline to be combined with other edges/curves
|
||||
|
||||
create text
|
||||
ideally, in various fonts.
|
||||
|
||||
3-d operations
|
||||
---------------------
|
||||
|
||||
rotation/transform that return a copy
|
||||
The current rotateAboutCenter and translate method modify the object, rather than returning a copy
|
||||
|
||||
primitive creation
|
||||
Need primitive creation for:
|
||||
* cone
|
||||
* sphere
|
||||
* cylinder
|
||||
* torus
|
||||
* wedge
|
||||
|
||||
extrude/cut up to surface
|
||||
allow a cut or extrude to terminate at another surface ,rather than either through all or a fixed distance
|
||||
|
||||
extrude along a path
|
||||
rather than just normal to the plane. This would include
|
||||
|
||||
loft
|
||||
create a feature between two or more wire sections
|
||||
|
||||
revolve
|
||||
revolve a wire around an axis to create a solid
|
||||
|
||||
STEP import
|
||||
allow embedding and importing step solids created in other tools, which
|
||||
can then be further manipulated parametrically
|
||||
|
||||
Dome
|
||||
very difficult to do otherwise
|
||||
|
||||
primitive boolean operations
|
||||
* intersect
|
||||
* union
|
||||
* subtract
|
||||
|
||||
|
||||
Algorithms
|
||||
---------------------
|
||||
|
||||
Wire Discretization
|
||||
Sample wires at point interval to improve closet wire computations
|
||||
|
||||
|
103
sphinxdoc/selectors.rst
Normal file
@ -0,0 +1,103 @@
|
||||
.. _selector_reference:
|
||||
|
||||
*************************
|
||||
CadQuery String Selectors
|
||||
*************************
|
||||
|
||||
.. automodule:: cadfile.cadutils.cadquery
|
||||
|
||||
CadQuery selector strings allow filtering various types of object lists. Most commonly, Edges, Faces, and Vertices are
|
||||
used, but all objects types can be filtered.
|
||||
|
||||
String selectors are used as arguments to the various selection methods:
|
||||
|
||||
* :py:meth:`CQ.faces`
|
||||
* :py:meth:`CQ.edges`
|
||||
* :py:meth:`CQ.vertices`
|
||||
* :py:meth:`CQ.solids`
|
||||
* :py:meth:`CQ.shells`
|
||||
|
||||
.. note::
|
||||
|
||||
String selectors are shortcuts to concrete selector classes, which you can use or extend. See
|
||||
:ref:`classreference` for more details
|
||||
|
||||
If you find that the built-in selectors are not sufficient, you can easily plug in your own.
|
||||
See :ref:`extending` to see how.
|
||||
|
||||
|
||||
|
||||
.. _filteringfaces:
|
||||
|
||||
Filtering Faces
|
||||
----------------
|
||||
|
||||
All types of filters work on faces. In most cases, the selector refers to the direction of the **normal vector**
|
||||
of the face.
|
||||
|
||||
.. warning::
|
||||
|
||||
If a face is not planar, selectors are evaluated at the center of mass of the face. This can lead
|
||||
to results that are quite unexpected.
|
||||
|
||||
The axis used in the listing below are for illustration: any axis would work similarly in each case.
|
||||
|
||||
========= ==================================== ====================================== ==========================
|
||||
Selector Selector Class Selects # objects returned
|
||||
========= ==================================== ====================================== ==========================
|
||||
+Z :py:class:`DirectionSelector` Faces with normal in +z direction 0 or 1
|
||||
\|Z :py:class:`ParallelDirSelector` Faces parallel to xy plane 0..many
|
||||
-X :py:class:`DirectionSelector` Faces with normal in neg x direction 0..many
|
||||
#Z :py:class:`PerpendicularDirSelector` Faces perpendicular to z direction 0..many
|
||||
%Plane :py:class:`TypeSelector` Faces of type plane 0..many
|
||||
>Y :py:class:`DirectionMinMaxSelector` Face farthest in the positive y dir 0 or 1
|
||||
<Y :py:class:`DirectionMinMaxSelector` Face farthest in the negative y dir 0 or 1
|
||||
========= ==================================== ====================================== ==========================
|
||||
|
||||
|
||||
.. _filteringedges:
|
||||
|
||||
Filtering Edges
|
||||
----------------
|
||||
|
||||
Some filter types are not supported for edges. The selector usually refers to the **direction** of the edge.
|
||||
|
||||
.. warning::
|
||||
|
||||
Non-linear edges are not selected for any selectors except type (%). Non-linear edges are never returned
|
||||
when these filters are applied.
|
||||
|
||||
The axis used in the listing below are for illustration: any axis would work similarly in each case.
|
||||
|
||||
========= ==================================== ===================================== ==========================
|
||||
Selector Selector Class Selects # objects returned
|
||||
========= ==================================== ===================================== ==========================
|
||||
+Z :py:class:`DirectionSelector` Edges aligned in the Z direction 0..many
|
||||
\|Z :py:class:`ParallelDirSelector` Edges parallel to z direction 0..many
|
||||
-X :py:class:`DirectionSelector` Edges aligned in neg x direction 0..many
|
||||
#Z :py:class:`PerpendicularDirSelector` Edges perpendicular to z direction 0..many
|
||||
%Line :py:class:`TypeSelector` Edges type line 0..many
|
||||
>Y :py:class:`DirectionMinMaxSelector` Edges farthest in the positive y dir 0 or 1
|
||||
<Y :py:class:`DirectionMinMaxSelector` Edges farthest in the negative y dir 0 or 1
|
||||
========= ==================================== ===================================== ==========================
|
||||
|
||||
|
||||
.. _filteringvertices:
|
||||
|
||||
Filtering Vertices
|
||||
-------------------
|
||||
|
||||
Only a few of the filter types apply to vertices. The location of the vertex is the subject of the filter
|
||||
|
||||
========= ==================================== ===================================== ==========================
|
||||
Selector Selector Class Selects # objects returned
|
||||
========= ==================================== ===================================== ==========================
|
||||
>Y :py:class:`DirectionMinMaxSelector` Edges farthest in the positive y dir 0 or 1
|
||||
<Y :py:class:`DirectionMinMaxSelector` Edges farthest in the negative y dir 0 or 1
|
||||
========= ==================================== ===================================== ==========================
|
||||
|
||||
Future Enhancements
|
||||
--------------------
|
||||
|
||||
* Support direct vectors inline, such as \|(x,y,z)
|
||||
* Support multiple selectors separated by spaces, which unions the results, such as "+Z +Y to select both z and y-most faces
|
43
sphinxdoc/themes/pparts/layout.html
Normal file
@ -0,0 +1,43 @@
|
||||
{% extends "basic/layout.html" %}
|
||||
|
||||
{%- block extrahead %}
|
||||
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Neuton&subset=latin" type="text/css" media="screen" charset="utf-8" />
|
||||
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Nobile:regular,italic,bold,bolditalic&subset=latin" type="text/css" media="screen" charset="utf-8" />
|
||||
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Open+Sans:400,800">
|
||||
<!--[if lte IE 6]>
|
||||
<link rel="stylesheet" href="{{ pathto('_static/ie6.css', 1) }}" type="text/css" media="screen" charset="utf-8" />
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-37657197-1']);
|
||||
_gaq.push(['_setDomainName', 'none']);
|
||||
_gaq.push(['_setAllowLinker', 'true']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
<![endif]-->
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{%- if logo %}
|
||||
<div class="header">
|
||||
<a href="{{ pathto(master_doc) }}">
|
||||
<div class="logo">
|
||||
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
||||
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{%- block sidebarlogo %}{%- endblock %}
|
||||
{%- block sidebarsourcelink %}{%- endblock %}
|
BIN
sphinxdoc/themes/pparts/static/body.jpg
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
sphinxdoc/themes/pparts/static/dialog-note.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
sphinxdoc/themes/pparts/static/dialog-seealso.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
sphinxdoc/themes/pparts/static/dialog-topic.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
sphinxdoc/themes/pparts/static/dialog-warning.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
310
sphinxdoc/themes/pparts/static/epub.css
Normal file
@ -0,0 +1,310 @@
|
||||
/*
|
||||
* default.css_t
|
||||
* ~~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- default theme.
|
||||
*
|
||||
* :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: {{ theme_bodyfont }};
|
||||
font-size: 100%;
|
||||
background-color: {{ theme_footerbgcolor }};
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: {{ theme_sidebarbgcolor }};
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 230px;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: {{ theme_bgcolor }};
|
||||
color: {{ theme_textcolor }};
|
||||
padding: 0 20px 30px 20px;
|
||||
}
|
||||
|
||||
{%- if theme_rightsidebar|tobool %}
|
||||
div.bodywrapper {
|
||||
margin: 0 230px 0 0;
|
||||
}
|
||||
{%- endif %}
|
||||
|
||||
div.footer {
|
||||
color: {{ theme_footertextcolor }};
|
||||
width: 100%;
|
||||
padding: 9px 0 9px 0;
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: {{ theme_footertextcolor }};
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.related {
|
||||
background-color: {{ theme_relbarbgcolor }};
|
||||
line-height: 30px;
|
||||
color: {{ theme_relbartextcolor }};
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: {{ theme_relbarlinkcolor }};
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
{%- if theme_stickysidebar|tobool %}
|
||||
top: 30px;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
{%- endif %}
|
||||
{%- if theme_rightsidebar|tobool %}
|
||||
float: right;
|
||||
{%- if theme_stickysidebar|tobool %}
|
||||
right: 0;
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
}
|
||||
|
||||
{%- if theme_stickysidebar|tobool %}
|
||||
/* this is nice, but it it leads to hidden headings when jumping
|
||||
to an anchor */
|
||||
/*
|
||||
div.related {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
margin-top: 30px;
|
||||
}
|
||||
*/
|
||||
{%- endif %}
|
||||
|
||||
div.sphinxsidebar h3 {
|
||||
font-family: {{ theme_headfont }};
|
||||
color: {{ theme_sidebartextcolor }};
|
||||
font-size: 1.4em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: {{ theme_sidebartextcolor }};
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: {{ theme_headfont }};
|
||||
color: {{ theme_sidebartextcolor }};
|
||||
font-size: 1.3em;
|
||||
font-weight: normal;
|
||||
margin: 5px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: {{ theme_sidebartextcolor }};
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.topless {
|
||||
margin: 5px 10px 10px 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px;
|
||||
padding: 0;
|
||||
color: {{ theme_sidebartextcolor }};
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: {{ theme_sidebarlinkcolor }};
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid {{ theme_sidebarlinkcolor }};
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
{% if theme_collapsiblesidebar|tobool %}
|
||||
/* for collapsible sidebar */
|
||||
div#sidebarbutton {
|
||||
background-color: {{ theme_sidebarbtncolor }};
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
/* -- hyperlink styles ------------------------------------------------------ */
|
||||
|
||||
a {
|
||||
color: {{ theme_linkcolor }};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: {{ theme_visitedlinkcolor }};
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
{% if theme_externalrefs|tobool %}
|
||||
a.external {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dashed {{ theme_linkcolor }};
|
||||
}
|
||||
|
||||
a.external:hover {
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
a.external:visited {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dashed {{ theme_visitedlinkcolor }};
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: {{ theme_headfont }};
|
||||
background-color: {{ theme_headbgcolor }};
|
||||
font-weight: normal;
|
||||
color: {{ theme_headtextcolor }};
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin: 20px -20px 10px -20px;
|
||||
padding: 3px 0 3px 10px;
|
||||
}
|
||||
|
||||
div.body h1 { margin-top: 0; font-size: 200%; }
|
||||
div.body h2 { font-size: 160%; }
|
||||
div.body h3 { font-size: 140%; }
|
||||
div.body h4 { font-size: 120%; }
|
||||
div.body h5 { font-size: 110%; }
|
||||
div.body h6 { font-size: 100%; }
|
||||
|
||||
a.headerlink {
|
||||
color: {{ theme_headlinkcolor }};
|
||||
font-size: 0.8em;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
background-color: {{ theme_headlinkcolor }};
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
text-align: justify;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.admonition p {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.admonition pre {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.admonition ul, div.admonition ol {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: #ffe4e4;
|
||||
border: 1px solid #f66;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 5px;
|
||||
background-color: {{ theme_codebgcolor }};
|
||||
color: {{ theme_codetextcolor }};
|
||||
line-height: 120%;
|
||||
border: 1px solid #ac9;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
padding: 0 1px 0 1px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #ede;
|
||||
}
|
||||
|
||||
.warning tt {
|
||||
background: #efc2c2;
|
||||
}
|
||||
|
||||
.note tt {
|
||||
background: #d6d6d6;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
font-family: {{ theme_bodyfont }};
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background-color: #f4debf;
|
||||
border-top: 1px solid #ac9;
|
||||
border-bottom: 1px solid #ac9;
|
||||
}
|
BIN
sphinxdoc/themes/pparts/static/footerbg.png
Normal file
After Width: | Height: | Size: 333 B |
BIN
sphinxdoc/themes/pparts/static/headerbg.png
Normal file
After Width: | Height: | Size: 203 B |
7
sphinxdoc/themes/pparts/static/ie6.css
Normal file
@ -0,0 +1,7 @@
|
||||
* html img,
|
||||
* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none",
|
||||
this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')",
|
||||
this.src = "_static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''),
|
||||
this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')",
|
||||
this.runtimeStyle.backgroundImage = "none")),this.pngSet=true)
|
||||
);}
|
BIN
sphinxdoc/themes/pparts/static/logo.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
sphinxdoc/themes/pparts/static/middlebg.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
387
sphinxdoc/themes/pparts/static/pparts.css_t
Normal file
@ -0,0 +1,387 @@
|
||||
/*
|
||||
* pylons.css_t
|
||||
* ~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- pylons theme.
|
||||
*
|
||||
* :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
html {background-image:url(../img/bg/html.png);}
|
||||
body {color:#333;background-position:50% -400px;background-repeat:repeat-x;}
|
||||
#home {background-position:50% 0;}
|
||||
body, input, textarea, button {font:13px/20px Arial,sans-serif;}
|
||||
|
||||
/* Color for the links */
|
||||
a {color:#84B51E; text-decoration:none;}
|
||||
|
||||
* {margin:0;}
|
||||
p, ul, ol, table, form, pre {margin-bottom:20px;}
|
||||
img {border:none;max-width:100%;}
|
||||
ul {list-style:none;}
|
||||
:focus {outline:0;}
|
||||
.clear {clear:both;}
|
||||
article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section {display:block;}
|
||||
.wrapper:after, #login:after {content:".";display:block;height:0;clear:both;visibility:hidden;}
|
||||
|
||||
/* Overall background color and image */
|
||||
html, #login {background-color:#f5f5f5;}
|
||||
body {background-image:url('../img/bg/body.jpg');}
|
||||
|
||||
/* Background and border color for drop-down navigation */
|
||||
nav ul ul {background-color:#13171A;border-color:#84B51E;}
|
||||
|
||||
/* Footer background color */
|
||||
footer, footer h3 span {background-color:#13171A;}
|
||||
|
||||
/* Primary navigation color */
|
||||
nav>ul>li>a {color:#ddd;}
|
||||
|
||||
/* Header colors */
|
||||
h1, h1 a {color:#13171A;}
|
||||
h2, h2 a, h3, h4, .pricing thead th {color:#444;}
|
||||
|
||||
|
||||
/* Color for the links */
|
||||
p a, .wrapper ul li a {color:#84B51E;}
|
||||
|
||||
body {
|
||||
font-family: 'Open Sans',Arial,sans-serif;
|
||||
font-size: 100%;
|
||||
background-color: #333;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 {{ theme_sidebarwidth }}px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
div.header {
|
||||
width:100%;
|
||||
height:142px;
|
||||
background: #eaad32 url(body.jpg) bottom;
|
||||
position:relative;
|
||||
}
|
||||
div.header ul > li > a{
|
||||
text-decoration:none;
|
||||
line-height:30px;
|
||||
font-size:15px;
|
||||
padding: 0 12p 0 13px;
|
||||
}
|
||||
div.header ul {
|
||||
background:none;
|
||||
}
|
||||
|
||||
div.logo {
|
||||
text-align: left;
|
||||
padding: 15px 40px;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 30px 30px;
|
||||
font-size: 1em;
|
||||
border-left: 1px solid #333;
|
||||
border-right-style: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
color: #ffffff;
|
||||
background-color:#13171A;
|
||||
width: 100%;
|
||||
padding: 13px 0;
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
background: transparent;
|
||||
clear:both;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.footer a:hover {
|
||||
color: #e88f00;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.related {
|
||||
position:absolute;
|
||||
top: 52px;
|
||||
width:100%;
|
||||
margin:0;
|
||||
list-style:none;
|
||||
line-height: 30px;
|
||||
color: #373839;
|
||||
font-size: 15px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: #1b61d6;
|
||||
}
|
||||
div.related h3{
|
||||
display:none;
|
||||
}
|
||||
|
||||
div.related ul {
|
||||
padding-left: 450px;
|
||||
}
|
||||
div.related li{
|
||||
display:none;
|
||||
}
|
||||
div.related li.right{
|
||||
display:inline;
|
||||
}
|
||||
div.related ul > li a{
|
||||
font-size: 30px;
|
||||
text-decoration: none;
|
||||
line-height:30px;
|
||||
color: #ddd;
|
||||
font-weight:bold;
|
||||
display:none;
|
||||
}
|
||||
div.related ul > li.right a{
|
||||
font-size: 15px;
|
||||
font-weight:normal;
|
||||
display:inline;
|
||||
}
|
||||
div.sphinxsidebar {
|
||||
font-size: 14px;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper{
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
color: #373839;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #444;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3{
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #888;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.topless {
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 20px;
|
||||
padding: 0;
|
||||
color: #373839;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #ccc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input[type=text]{
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* -- sidebars -------------------------------------------------------------- */
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em;
|
||||
border: 2px solid #c6d880;
|
||||
background-color: #e6efc2;
|
||||
width: 40%;
|
||||
float: right;
|
||||
border-right-style: none;
|
||||
border-left-style: none;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
p.sidebar-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: 'Open Sans',Arial,sans-serif;
|
||||
background-color: #ffffff;
|
||||
font-weight: normal;
|
||||
color: #444;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
|
||||
div.body h2 { font-size: 150%; background-color: #ffffff; }
|
||||
div.body h3 { font-size: 120%; background-color: #ffffff; }
|
||||
div.body h4 { font-size: 110%; background-color: #ffffff; }
|
||||
div.body h5 { font-size: 100%; background-color: #ffffff; }
|
||||
div.body h6 { font-size: 100%; background-color: #ffffff; }
|
||||
|
||||
a.headerlink {
|
||||
color: #1b61d6;
|
||||
font-size: 0.8em;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.highlight{
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
div.note {
|
||||
border: 2px solid #7a9eec;
|
||||
border-right-style: none;
|
||||
border-left-style: none;
|
||||
padding: 10px 20px 10px 60px;
|
||||
background: #e1ecfe url(dialog-note.png) no-repeat 10px 8px;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background: #fff6bf url(dialog-seealso.png) no-repeat 10px 8px;
|
||||
border: 2px solid #ffd324;
|
||||
border-left-style: none;
|
||||
border-right-style: none;
|
||||
padding: 10px 20px 10px 60px;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background: #eeeeee;
|
||||
border: 2px solid #C6C9CB;
|
||||
padding: 10px 20px;
|
||||
border-right-style: none;
|
||||
border-left-style: none;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background: #fbe3e4 url(dialog-warning.png) no-repeat 10px 8px;
|
||||
border: 2px solid #fbc2c4;
|
||||
border-right-style: none;
|
||||
border-left-style: none;
|
||||
padding: 10px 20px 10px 60px;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
background-color: #fafafa;
|
||||
color: #222;
|
||||
line-height: 1.2em;
|
||||
border: 2px solid #C6C9CB;
|
||||
font-size: 1.1em;
|
||||
margin: 1.5em 0 1.5em 0;
|
||||
border-right-style: none;
|
||||
border-left-style: none;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: transparent;
|
||||
color: #222;
|
||||
font-size: 1.1em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
font-family: "Nobile", sans-serif;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background-color: #fff6bf;
|
||||
border: 2px solid #ffd324;
|
||||
border-left-style: none;
|
||||
border-right-style: none;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
table.highlighttable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.highlighttable td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a em.std-term {
|
||||
color: #007f00;
|
||||
}
|
||||
|
||||
a:hover em.std-term {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.download {
|
||||
font-family: "Nobile", sans-serif;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
tt.xref {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
BIN
sphinxdoc/themes/pparts/static/transparent.gif
Normal file
After Width: | Height: | Size: 49 B |
4
sphinxdoc/themes/pparts/theme.conf
Normal file
@ -0,0 +1,4 @@
|
||||
[theme]
|
||||
inherit = basic
|
||||
stylesheet = pparts.css
|
||||
pygments_style = friendly
|
196
tests/TestCQSelectors.py
Normal file
@ -0,0 +1,196 @@
|
||||
__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 TestBase import *
|
||||
from cadquery import *
|
||||
|
||||
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(ParallelDirSelector(Vector((0,0,1)))).size()) #same thing as above
|
||||
self.assertEqual(2, c.faces(ParallelDirSelector(Vector((0,0,-1)))).size()) #same thing as above
|
||||
|
||||
#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)
|
||||
|
||||
def testMinDistance(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(0.0,v.Z,3)
|
||||
|
||||
def testNearestTo(self):
|
||||
c = CQ(makeUnitCube())
|
||||
|
||||
#nearest vertex to origin is (0,0,0)
|
||||
t = Vector(0.1,0.1,0.1)
|
||||
|
||||
v = c.vertices(NearestToPointSelector(t)).vals()[0]
|
||||
self.assertTupleAlmostEquals((0.0,0.0,0.0),(v.X,v.Y,v.Z),3)
|
||||
|
||||
t = Vector(0.1,0.1,0.2)
|
||||
#nearest edge is the vertical side edge, 0,0,0 -> 0,0,1
|
||||
e = c.edges(NearestToPointSelector(t)).vals()[0]
|
||||
v = c.edges(NearestToPointSelector(t)).vertices().vals()
|
||||
self.assertEqual(2,len(v))
|
||||
|
||||
#nearest solid is myself
|
||||
s = c.solids(NearestToPointSelector(t)).vals()
|
||||
self.assertEqual(1,len(s))
|
||||
|
||||
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("<XY")
|
||||
self.assertEqual(1,v2.size() ) #another way
|
||||
#make sure the vertex is the right one
|
||||
|
||||
self.assertTupleAlmostEquals((0.0,0.0,1.0),v2.val().toTuple() ,3)
|
59
tests/TestCadObjects.py
Normal file
@ -0,0 +1,59 @@
|
||||
#system modules
|
||||
import sys
|
||||
|
||||
#my modules
|
||||
|
||||
import from cadquery import *
|
||||
|
||||
|
||||
class TestCadObjects(BaseTest):
|
||||
|
||||
def testVectorConstructors(self):
|
||||
v1 = Vector(1,2,3)
|
||||
v2 = Vector((1,2,3))
|
||||
v3 = Vector(FreeCAD.Base.Vector(1,2,3))
|
||||
|
||||
for v in [v1,v2,v3]:
|
||||
self.assertTupleAlmostEquals((1,2,3),v.toTuple(),4)
|
||||
|
||||
def testVertex(self):
|
||||
"""
|
||||
Tests basic vertex functions
|
||||
"""
|
||||
v = Vertex( FreeCAD.Part.Vertex(1,1,1) )
|
||||
self.assertEqual(1,v.X)
|
||||
self.assertEquals(Vector,type(v.Center() ))
|
||||
|
||||
def testBasicBoundingBox(self):
|
||||
v = Vertex( FreeCAD.Part.Vertex(1,1,1))
|
||||
v2 = Vertex( FreeCAD.Part.Vertex(2,2,2))
|
||||
self.assertEquals(BoundBox,type(v.BoundingBox()))
|
||||
self.assertEquals(BoundBox,type(v2.BoundingBox()))
|
||||
|
||||
bb1 = v.BoundingBox().add(v2.BoundingBox())
|
||||
|
||||
self.assertEquals(bb1.xlen,1.0)
|
||||
|
||||
def testEdgeWrapperCenter(self):
|
||||
e = Edge( FreeCAD.Part.makeCircle(2.0,FreeCAD.Base.Vector(1,2,3)) )
|
||||
|
||||
self.assertTupleAlmostEquals((1.0,2.0,3.0),e.Center().toTuple(),3)
|
||||
|
||||
def testDot(self):
|
||||
v1 = Vector(2,2,2)
|
||||
v2 = Vector(1,-1,1)
|
||||
self.assertEquals(2.0,v1.dot(v2))
|
||||
|
||||
def testVectorAdd(self):
|
||||
result = Vector(1,2,0) + Vector(0,0,3)
|
||||
self.assertTupleAlmostEquals((1.0,2.0,3.0),result.toTuple(),3)
|
||||
|
||||
def testTranslate(self):
|
||||
e = Shape.cast( FreeCAD.Part.makeCircle(2.0,FreeCAD.Base.Vector(1,2,3)) )
|
||||
e2 = e.translate(Vector(0,0,1))
|
||||
|
||||
self.assertTupleAlmostEquals((1.0,2.0,4.0),e2.Center().toTuple(),3)
|
||||
|
||||
def testVertices(self):
|
||||
e = Shape.cast(FreeCAD.Part.makeLine((0,0,0),(1,1,0)))
|
||||
self.assertEquals(2,len(e.Vertices()))
|
838
tests/TestCadQuery.py
Normal file
@ -0,0 +1,838 @@
|
||||
"""
|
||||
This module tests cadquery creation and manipulation functions
|
||||
|
||||
"""
|
||||
#system modules
|
||||
import math,sys,os.path,time
|
||||
|
||||
#my modules
|
||||
from cadquery import *
|
||||
|
||||
#where unit test output will be saved
|
||||
OUTDIR = "c:/temp"
|
||||
SUMMARY_FILE = os.path.join(OUTDIR,"testSummary.html")
|
||||
|
||||
SUMMARY_TEMPLATE="""<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
.testResult{
|
||||
background: #eeeeee;
|
||||
margin: 50px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!--TEST_CONTENT-->
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
TEST_RESULT_TEMPLATE="""
|
||||
<div class="testResult"><h3>%(name)s</h3>
|
||||
%(svg)s
|
||||
</div>
|
||||
<!--TEST_CONTENT-->
|
||||
"""
|
||||
|
||||
#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('<?xml version="1.0" encoding="UTF-8" standalone="no"?>',"")
|
||||
|
||||
#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_CONTENT-->", 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 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())
|
||||
|
||||
|
||||
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 testLoft(self):
|
||||
"""
|
||||
Test making a lofted solid
|
||||
:return:
|
||||
"""
|
||||
s = Workplane("XY").circle(4.0).workplane(5.0).rect(2.0,2.0).loft()
|
||||
self.saveModel(s)
|
||||
#the result should have 7 faces
|
||||
self.assertEqual(1,s.solids().size())
|
||||
|
||||
#the resulting loft had a split on the side, not sure why really, i expected only 3 faces
|
||||
self.assertEqual(7,s.faces().size() )
|
||||
|
||||
def testLoftCombine(self):
|
||||
"""
|
||||
test combining a lof with another feature
|
||||
:return:
|
||||
"""
|
||||
s = Workplane("front").box(4.0,4.0,0.25).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 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").shell(-1.0* t) #shell inwards not outwards
|
||||
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("<Z").workplane(invert=True) #this is cheating a little-- how to select the inner face from the shell?
|
||||
|
||||
if lbumps > 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").vertices().item(0).val().Z, 3 )
|
||||
|
||||
#TODO: it might be nice to provide a version of translate that modifies the existing geometry too
|
||||
d = c.translate(Vector(0,0,1.5))
|
||||
self.assertAlmostEqual(1.5,d.faces("<Z").vertices().item(0).val().Z, 3 )
|
||||
|
||||
def testTranslateWire(self):
|
||||
c = CQ(makeUnitSquareWire())
|
||||
self.assertAlmostEqual(0.0,c.edges().vertices().item(0).val().Z, 3 )
|
||||
d = c.translate(Vector(0,0,1.5))
|
||||
self.assertAlmostEqual(1.5,d.edges().vertices().item(0).val().Z, 3 )
|
||||
|
||||
def testSolidReferencesCombine(self):
|
||||
"test that solid references are updated correctly"
|
||||
c = CQ( makeUnitCube()) #the cube is the context solid
|
||||
self.assertEqual(6,c.faces().size()) #cube has six faces
|
||||
|
||||
r = c.faces().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 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 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"
|
||||
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('c:/temp/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('c:/temp/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('c:/temp/testBasicLinesZ.STEP')
|
||||
|
||||
self.saveModel(r)
|
||||
|
||||
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 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 createing 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):
|
||||
"test that a simple sphere 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 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,.75)
|
||||
self.assertEquals(18,c.faces().size() )
|
||||
self.saveModel(c)
|
||||
|
||||
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 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))
|
||||
|
||||
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 beause 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, becaue 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 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)
|
||||
|
||||
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 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 testTwistedGear3(self):
|
||||
pts = plugins.make_gear(14.5,10,2.5) #make involutes
|
||||
s = Workplane("XY").polyline(pts).twistExtrude(4.0,8.0)
|
||||
#s2 = s.faces(">Z").workplane().transformed(rotate=Vector(0,0,8)).polyline(pts).twistExtrude(4.0,-8.0,combine=False)
|
||||
#s3 = s.union(s2)
|
||||
#s.val().exportStl("c:\\temp\\pleasework3.stl")
|
||||
#s3.val().exportStl("c:\\temp\\pleasework5.stl")
|
||||
self.saveModel(s)
|
||||
|
||||
|
||||
def testEnclosure(self):
|
||||
"""
|
||||
Builds an electronics enclosure
|
||||
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,True)\
|
||||
.rect((p_outerWidth - 2.0* p_thickness),(p_outerLength - 2.0*p_thickness))\
|
||||
.extrude((p_outerHeight - 2.0*p_thickness),False) #set combine false to produce just the new boss
|
||||
ishell.edges("|Z").fillet(p_sideRadius - p_thickness)
|
||||
|
||||
#make the box outer box
|
||||
box = oshell.cut(ishell)
|
||||
|
||||
#make the screwposts
|
||||
POSTWIDTH = (p_outerWidth - 2.0*p_screwpostInset)
|
||||
POSTLENGTH = (p_outerLength -2.0*p_screwpostInset)
|
||||
|
||||
postCenters = box.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)
|
||||
|
||||
|
44
tests/TestJsonMesher.py
Normal file
@ -0,0 +1,44 @@
|
||||
import unittest
|
||||
from JsonUtils import JsonMesh
|
||||
import FreeCAD
|
||||
from FreeCAD import Part
|
||||
from FreeCAD import Vector
|
||||
|
||||
"""
|
||||
WARNING: set FREECAD_HOME for these tests to work!
|
||||
"""
|
||||
class TestJSonModel(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.mesh = JsonMesh();
|
||||
|
||||
def testOneFace(self):
|
||||
mesh = self.mesh;
|
||||
mesh.addVertex(0.0,0.0,0.0);
|
||||
mesh.addVertex(1.0,1.0,0.0);
|
||||
mesh.addVertex(-1.0,1.0,0);
|
||||
mesh.addTriangleFace(0, 1, 2);
|
||||
self.assertEqual(3*3,len(mesh.vertices));
|
||||
self.assertEqual(1+3,len(mesh.faces));
|
||||
|
||||
def testSphere(self):
|
||||
|
||||
#make a sphere
|
||||
p = Part.makeSphere(2.0);
|
||||
t = p.tessellate(0.01); #a pretty fine mesh
|
||||
|
||||
#add vertices
|
||||
for vec in t[0]:
|
||||
self.mesh.addVertex(vec.x, vec.y, vec.z);
|
||||
|
||||
#add faces
|
||||
for f in t[1]:
|
||||
self.mesh.addTriangleFace(f[0],f[1], f[2]);
|
||||
|
||||
#make resulting json
|
||||
self.mesh.buildTime = 0.1;
|
||||
js = self.mesh.toJson();
|
||||
|
||||
#make sure the mesh has like >1000 vertices
|
||||
self.assertTrue(self.mesh.nVertices > 1000);
|
||||
self.assertTrue(self.mesh.nFaces > 1000);
|
16
tests/TestSVGexporter.py
Normal file
@ -0,0 +1,16 @@
|
||||
__author__ = 'dcowden'
|
||||
|
||||
from cadquery import *
|
||||
|
||||
import unittest,sys
|
||||
import MakeTestObjects
|
||||
import SVGexporter
|
||||
|
||||
class TestCadQuery(unittest.TestCase):
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def testExport(self):
|
||||
t = MakeTestObjects.makeCube(20)
|
||||
|
||||
SVGexporter.exportSVG(t,'c:/temp/test.svg')
|
84
tests/TestWorkplanes.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""
|
||||
Tests basic workplane functionality
|
||||
"""
|
||||
#core modules
|
||||
|
||||
#my modules
|
||||
from cadquery import *
|
||||
|
||||
class TestPlane(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 )
|
47
tests/__init__.py
Normal file
@ -0,0 +1,47 @@
|
||||
from cadquery import *
|
||||
import unittest
|
||||
import sys
|
||||
FREECAD_LIB = "c:/apps/FreeCAD0.12/bin";
|
||||
sys.path.append(FREECAD_LIB);
|
||||
import FreeCAD
|
||||
|
||||
P = FreeCAD.Part
|
||||
V = FreeCAD.Base.Vector
|
||||
|
||||
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) == 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)
|