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