cadquery-freecad-module/cadquery/workplane.py
2013-04-16 22:29:06 -04:00

1425 lines
56 KiB
Python

"""
Copyright (C) 2011-2013 Parametric Products Intellectual Holdings, LLC
This file is part of CadQuery.
CadQuery is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
CadQuery is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; If not, see <http://www.gnu.org/licenses/>
"""
import math
from cadquery import Vector,CQ,CQContext,Plane,Wire
class Workplane(CQ):
"""
Defines a coordinate system in space, in which 2-d coordinates can be used.
:param plane: the plane in which the workplane will be done
:type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
:param origin: the desired origin of the new workplane
:type origin: a 3-tuple in global coordinates, or None to default to the origin
:param obj: an object to use initially for the stack
:type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
:raises: ValueError if the provided plane is not a plane, a valid named workplane
:return: A Workplane object, with coordinate system matching the supplied plane.
The most common use is::
s = Workplane("XY")
After creation, the stack contains a single point, the origin of the underlying plane, and the
*current point* is on the origin.
.. note::
You can also create workplanes on the surface of existing faces using
:py:meth:`CQ.workplane`
"""
FOR_CONSTRUCTION = 'ForConstruction'
def __init__(self, inPlane ,origin=(0,0,0), obj=None):
"""
make a workplane from a particular plane
:param plane: the plane in which the workplane will be done
:type plane: a Plane object, or a string in (XY|YZ|XZ|front|back|top|bottom|left|right)
:param origin: the desired origin of the new workplane
:type origin: a 3-tuple in global coordinates, or None to default to the origin
:param obj: an object to use initially for the stack
:type obj: a CAD primitive, or None to use the centerpoint of the plane as the initial stack value.
:raises: ValueError if the provided plane is not a plane, or one of XY|YZ|XZ
:return: A Workplane object, with coordinate system matching the supplied plane.
The most common use is::
s = Workplane("XY")
After creation, the stack contains a single point, the origin of the underlying plane, and the
*current point* is on the origin.
"""
if inPlane.__class__.__name__ == 'Plane':
tmpPlane = inPlane
elif type(inPlane) == str:
tmpPlane = Plane.named(inPlane,origin)
else:
tmpPlane = None
if tmpPlane == None:
raise ValueError(" Provided value %s is not a valid work plane." % str(inPlane))
self.obj = obj
self.plane = tmpPlane
self.firstPoint = None
self.objects = [self.plane.origin] #changed so that workplane has the center as the first item on the stack
self.parent = None
self.ctx = CQContext()
def transformed(self,rotate=Vector(0,0,0),offset=Vector(0,0,0)):
"""
Create a new workplane based on the current one.
The origin of the new plane is located at the existing origin+offset vector, where offset is given in
coordinates local to the current plane
The new plane is rotated through the angles specified by the components of the rotation vector
:param rotate: vector of angles to rotate, in degrees relative to work plane coordinates
:param offset: vector to offset the new plane, in local work plane coordinates
:return: a new work plane, transformed as requested
"""
p = self.plane.rotated(rotate)
p.setOrigin3d(self.plane.toWorldCoords(offset.toTuple() ))
ns = self.newObject([p.origin])
ns.plane = p
return ns
def newObject(self,objlist):
"""
Create a new workplane object from this one.
Overrides CQ.newObject, and should be used by extensions, plugins, and
subclasses to create new objects.
:param objlist: new objects to put on the stack
:type objlist: a list of CAD primitives
:return: a new Workplane object with the current workplane as a parent.
"""
#copy the current state to the new object
ns = Workplane("XY")
ns.plane = self.plane
ns.parent = self
ns.objects = list(objlist)
ns.ctx = self.ctx
return ns
def _findFromPoint(self,useLocalCoords=False):
"""
finds the start point for an operation when an existing point
is implied. Examples include 2d operations such as lineTo,
which allows specifying the end point, and implicitly use the
end of the previous line as the starting point
:return: a Vector representing the point to use, or none if
such a point is not available.
:param useLocalCoords: selects whether the point is returned
in local coordinates or global coordinates.
The algorithm is this:
* If an Edge is on the stack, its end point is used.yp
* if a vector is on the stack, it is used
WARNING: only the first object on the stack is used.
NOTE:
"""
obj = self.objects[0]
p = None
if isinstance(obj,Edge):
p = obj.endPoint()
elif isinstance(obj,Vector):
p = obj
else:
raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj))
if useLocalCoords:
return self.plane.toLocalCoords(p)
else:
return p
def rarray(self,xSpacing,ySpacing,xCount,yCount,center=True):
"""
Creates an array of points and pushes them onto the stack.
If you want to position the array at another point, create another workplane
that is shifted to the position you would like to use as a reference
:param xSpacing: spacing between points in the x direction ( must be > 0)
:param ySpacing: spacing between points in the y direction ( must be > 0)
:param xCount: number of points ( > 0 )
:param yCount: number of poitns ( > 0 )
:param center: if true, the array will be centered at the center of the workplane. if false, the lower
left corner will be at the center of the work plane
"""
if xSpacing < 1 or ySpacing < 1 or xCount < 1 or yCount < 1:
raise ValueError("Spacing and count must be > 0 ")
lpoints = [] #coordinates relative to bottom left point
for x in range(xCount):
for y in range(yCount):
lpoints.append( (xSpacing*(x), ySpacing*(y)) )
#shift points down and left relative to origin if requested
if center:
xc = xSpacing*(xCount-1) * 0.5
yc = ySpacing*(yCount-1) * 0.5
cpoints = []
for p in lpoints:
cpoints.append( ( p[0] - xc, p[1] - yc ))
lpoints = list(cpoints)
return self.pushPoints(lpoints)
def pushPoints(self,pntList):
"""
Pushes a list of points onto the stack as vertices.
The points are in the 2-d coordinate space of the workplane face
:param pntList: a list of points to push onto the stack
:type pntList: list of 2-tuples, in *local* coordinates
:return: a new workplane with the desired points on the stack.
A common use is to provide a list of points for a subsequent operation, such as creating circles or holes.
This example creates a cube, and then drills three holes through it, based on three points::
s = Workplane().box(1,1,1).faces(">Z").workplane().pushPoints([(-0.3,0.3),(0.3,0.3),(0,0)])
body = s.circle(0.05).cutThruAll()
Here the circle function operates on all three points, and is then extruded to create three holes.
See :py:meth:`circle` for how it works.
"""
vecs = []
for pnt in pntList:
vec = self.plane.toWorldCoords(pnt)
vecs.append(vec)
return self.newObject(vecs)
def center(self,x,y):
"""
Shift local coordinates to the specified location.
The location is specified in terms of local coordinates.
:param float x: the new x location
:param float y: the new y location
:returns: the workplane object, with the center adjusted.
The current point is set to the new center.
This method is useful to adjust the center point after it has been created automatically on a face,
but not where you'd like it to be.
In this example, we adjust the workplane center to be at the corner of a cube, instead of
the center of a face, which is the default::
#this workplane is centered at x=0.5,y=0.5, the center of the upper face
s = Workplane().box(1,1,1).faces(">Z").workplane()
s.center(-0.5,-0.5) # move the center to the corner
t = s.circle(0.25).extrude(0.2)
assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner
The result is a cube with a round boss on the corner
"""
"Shift local coordinates to the specified location, according to current coordinates"
self.plane.setOrigin2d(x,y)
n = self.newObject([self.plane.origin])
return n
def lineTo(self, x, y,forConstruction=False):
"""
Make a line from the current point to the provided point
:param float x: the x point, in workplane plane coordinates
:param float y: the y point, in workplane plane coordinates
:return: the Workplane object with the current point at the end of the new line
see :py:meth:`line` if you want to use relative dimensions to make a line instead.
"""
startPoint = self._findFromPoint(False)
endPoint = self.plane.toWorldCoords((x, y))
p = Edge.makeLine(startPoint,endPoint)
if not forConstruction:
self._addPendingEdge(p)
return self.newObject([p])
#line a specified incremental amount from current point
def line(self, xDist, yDist ,forConstruction=False):
"""
Make a line from the current point to the provided point, using
dimensions relative to the current point
:param float xDist: x distance from current point
:param float yDist: y distance from current point
:return: the workplane object with the current point at the end of the new line
see :py:meth:`lineTo` if you want to use absolute coordinates to make a line instead.
"""
p = self._findFromPoint(True) #return local coordinates
return self.lineTo(p.x + xDist, yDist + p.y,forConstruction)
def vLine(self, distance,forConstruction=False):
"""
Make a vertical line from the current point the provided distance
:param float distance: (y) distance from current point
:return: the workplane object with the current point at the end of the new line
"""
return self.line(0, distance,forConstruction)
def vLineTo(self,yCoord,forConstruction=False):
"""
Make a vertcial line from the current point to the provided y coordinate.
Useful if it is more convienient to specify the end location rather than distance,
as in :py:meth:`vLine`
:param float yCoord: y coordinate for the end of the line
:return: the Workplane object with the current point at the end of the new line
"""
p = self._findFromPoint(True)
return self.lineTo(p.x,yCoord,forConstruction)
def hLineTo(self,xCoord,forConstruction=False):
"""
Make a horizontal line from the curren tpoint to the provided x coordinate.
Useful if it is more convienient to specify the end location rather than distance,
as in :py:meth:`hLine`
:param float xCoord: x coordinate for the end of the line
:return: the Workplane object with the current point at the end of the new line
"""
p = self._findFromPoint(True)
return self.lineTo(xCoord,p.y,forConstruction)
def hLine(self, distance,forConstruction=False):
"""
Make a horizontal line from the current point the provided distance
:param float distance: (x) distance from current point
:return: the Workplane object with the current point at the end of the new line
"""
return self.line(distance, 0,forConstruction)
#absolute move in current plane, not drawing
def moveTo(self, x=0, y=0):
"""
Move to the specified point, without drawing.
:param x: desired x location, in local coordinates
:type x: float, or none for zero
:param y: desired y location, in local coorindates
:type y: float, or none for zero.
Not to be confused with :py:meth:`center`, which moves the center of the entire
workplane, this method only moves the current point ( and therefore does not affect objects
already drawn ).
See :py:meth:`move` to do the same thing but using relative dimensions
"""
newCenter = Vector(x,y,0)
return self.newObject([self.plane.toWorldCoordinates(newCenter)])
#relative move in current plane, not drawing
def move(self, xDist=0, yDist=0):
"""
Move the specified distance from the current point, without drawing.
:param xDist: desired x distance, in local coordinates
:type xDist: float, or none for zero
:param yDist: desired y distance, in local coorindates
:type yDist: float, or none for zero.
Not to be confused with :py:meth:`center`, which moves the center of the entire
workplane, this method only moves the current point ( and therefore does not affect objects
already drawn ).
See :py:meth:`moveTo` to do the same thing but using absolute coordinates
"""
p = self._findFromPoint(True)
newCenter = p + Vector(xDist,yDist,0)
return self.newObject([self.plane.toWorldCoordinates(newCenter)])
def spline(self,listOfXYTuple,forConstruction=False):
"""
Create a spline interpolated through the provided points.
:param listOfXYTuple: points to interpolate through
:type listOfXYTuple: list of 2-tuple
:return: a Workplane object with the current point at the end of the spline
The spline will begin at the current point, and
end with the last point in the XY typle list
This example creates a block with a spline for one side::
s = Workplane(Plane.XY())
sPnts = [
(2.75,1.5),
(2.5,1.75),
(2.0,1.5),
(1.5,1.0),
(1.0,1.25),
(0.5,1.0),
(0,1.0)
]
r = s.lineTo(3.0,0).lineTo(3.0,1.0).spline(sPnts).close()
r = r.extrude(0.5)
*WARNING* It is fairly easy to create a list of points
that cannot be correctly interpreted as a spline.
Future Enhancements:
* provide access to control points
"""
gstartPoint = self._findFromPoint(False)
gEndPoint = self.plane.toWorldCoords(listOfXYTuple[-1])
vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
allPoints = [gstartPoint] + vecs
e = Edge.makeSpline(allPoints)
if not forConstruction:
self._addPendingEdge(e)
return self.newObject([e])
def threePointArc(self,point1, point2,forConstruction=False):
"""
Draw an arc from the current point, through point1, and ending at point2
:param point1: point to draw through
:type point1: 2-tuple, in workplane coordinates
:param point2: end point for the arc
:type point2: 2-tuple, in workplane coordinates
:return: a workplane with the current point at the end of the arc
Future Enhancments:
provide a version that allows an arc using relative measures
provide a centerpoint arc
provide tangent arcs
"""
gstartPoint = self._findFromPoint(False)
gpoint1 = self.plane.toWorldCoords(point1)
gpoint2 = self.plane.toWorldCoords(point2)
arc = Edge.makeThreePointArc(gstartPoint,gpoint1,gpoint2)
if not forConstruction:
self._addPendingEdge(arc)
return self.newObject([arc])
def rotateAndCopy(self,matrix):
"""
Makes a copy of all edges on the stack, rotates them according to the
provided matrix, and then attempts to consolidate them into a single wire.
:param matrix: a 4xr transformation matrix, in global coordinates
:type matrix: a FreeCAD Base.Matrix object
:return: a cadquery object with consolidated wires, and any originals on the stack.
The most common use case is to create a set of open edges, and then mirror them
around either the X or Y axis to complete a closed shape.
see :py:meth:`mirrorX` and :py:meth:`mirrorY` to mirror about the global X and Y axes
see :py:meth:`mirrorX` and for an example
Future Enhancements:
faster implementation: this one transforms 3 times to accomplish the result
"""
#compute rotation matrix ( global --> local --> rotate --> global )
rm = self.plane.fG.multiply(matrix).multiply(self.plane.rG)
#convert edges to a wire, if there are pending edges
n = self.wire(forConstruction=False)
#attempt to consolidate wires together.
consolidated = n.consolidateWires()
#ok, mirror all the wires.
#There might be a better way, but to do this rotation takes 3 steps
#transform geometry to local coordinates
#then rotate about x
#then transform back to global coordiante
originalWires = consolidated.wires().vals()
for w in originalWires:
mirrored = w.transformGeometry(rm)
consolidated.objects.append(mirrored)
consolidated._addPendingWire(mirrored)
#attempt again to consolidate all of the wires
c = consolidated.consolidateWires()
#c = consolidated
return c
def mirrorY(self):
"""
Mirror entities around the y axis of the workplane plane.
:return: a new object with any free edges consolidated into as few wires as possible.
All free edges are collected into a wire, and then the wire is mirrored,
and finally joined into a new wire
Typically used to make creating wires with symmetry easier. This line of code::
s = Workplane().lineTo(2,2).threePointArc((3,1),(2,0)).mirrorX().extrude(0.25)
Produces a flat, heart shaped object
Future Enhancements:
mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
"""
tm = Base.Matrix()
tm.rotateY(math.pi)
return self.rotateAndCopy(tm)
def mirrorX(self):
"""
Mirror entities around the x axis of the workplane plane.
:return: a new object with any free edges consolidated into as few wires as possible.
All free edges are collected into a wire, and then the wire is mirrored,
and finally joined into a new wire
Typically used to make creating wires with symmetry easier.
Future Enhancements:
mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
"""
tm = Base.Matrix()
tm.rotateX(math.pi)
return self.rotateAndCopy(tm)
def _addPendingEdge(self,edge):
"""
Queues an edge for later combination into a wire.
:param edge:
:return:
"""
self.ctx.pendingEdges.append(edge)
if self.ctx.firstPoint is None:
self.ctx.firstPoint = edge.startPoint()
def _addPendingWire(self,wire):
"""
Queue a Wire for later extrusion
Internal Processing Note. In FreeCAD, edges-->wires-->faces-->solids.
but users do not normally care about these distinctions. Users 'think' in terms
of edges, and solids.
CadQuery tracks edges as they are drawn, and automatically combines them into wires
when the user does an operation that needs it.
Similarly, cadQuery tracks pending wires, and automaticlaly combines them into faces
when necessary to make a solid.
"""
self.ctx.pendingWires.append(wire)
def consolidateWires(self):
"""
Attempt to consolidate wires on the stack into a single.
If possible, a new object with the results are returned.
if not possible, the wires remain separated
FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly somtimes
Additionally, it has a bug where a profile compose of two wires ( rathre than one )
also does not work properly
together these are a real problem.
"""
wires = self.wires().vals()
if len(wires) < 2:
return self
#TODO: this makes the assumption that either all wires could be combined, or none.
#in reality trying each combination of wires is probably not reasonable anyway
w = Wire.combine(wires)
#ok this is a little tricky. if we consolidate wires, we have to actually
#modify the pendingWires collection to remove the original ones, and replace them
#with the consolidate done
#since we are already assuming that all wires could be consolidated, its easy, we just
#clear the pending wire list
r = self.newObject([w])
r.ctx.pendingWires = []
r._addPendingWire(w)
return r
def wire(self,forConstruction=False):
"""
Returns a CQ object with all pending edges connected into a wire.
All edges on the stack that can be combined will be combined into a single wire object,
and other objects will remain on the stack unmodified
:param forConstruction: whether the wire should be used to make a solid, or if it is just for reference
:type forConstruction: boolean. true if the object is only for reference
This method is primarily of use to plugin developers making utilites for 2-d construction. This method
shoudl be called when a user operation implies that 2-d construction is finished, and we are ready to
begin working in 3d
SEE '2-d construction concepts' for a more detailed explanation of how cadquery handles edges, wires, etc
Any non edges will still remain.
"""
edges = self.ctx.pendingEdges
#do not consolidate if there are no free edges
if len(edges) == 0:
return self
self.ctx.pendingEdges = []
others = []
for e in self.objects:
if type(e) != Edge:
others.append(e)
w = Wire.assembleEdges(edges)
if not forConstruction:
self._addPendingWire(w)
return self.newObject(others + [w])
def each(self,callBackFunction,useLocalCoordinates=False):
"""
runs the provided function on each value in the stack, and collects the return values into a new CQ
object.
Special note: a newly created workplane always has its center point as its only stack item
:param callBackFunction: the function to call for each item on the current stack.
:param useLocalCoordinates: should values be converted from local coordinates first?
:type useLocalCoordinates: boolean
The callback function must accept one argument, which is the item on the stack, and return
one object, which is collected. If the function returns None, nothing is added to the stack.
The object passed into the callBackFunction is potentially transformed to local coordinates, if
useLocalCoordinates is true
useLocalCoordinates is very useful for plugin developers.
If false, the callback function is assumed to be working in global coordinates. Objects created are added
as-is, and objects passed into the function are sent in using global coordinates
If true, the calling function is assumed to be working in local coordinates. Objects are transformed
to local coordinates before they are passed into the callback method, and result objects are transformed
to global coorindates after they are returned.
This allows plugin developers to create objects in local coordinates, without worrying
about the fact that the working plane is different than the global coordinate system.
TODO: wrapper object for Wire will clean up forConstruction flag everywhere
"""
results = []
for obj in self.objects:
if useLocalCoordinates:
#TODO: this needs to work for all types of objects, not just vectors!
r = callBackFunction(self.plane.toLocalCoords(obj))
r = r.transformShape(self.plane.rG)
else:
r = callBackFunction(obj)
if type(r) == Wire:
if not r.forConstruction:
self._addPendingWire(r)
results.append ( r )
return self.newObject(results)
def eachpoint(self,callbackFunction, useLocalCoordinates=False):
"""
Same as each(), except each item on the stack is converted into a point before it
is passed into the callback function.
:return: CadQuery object which contains a list of vectors (points ) on its stack.
:param useLocalCoordinates: should points be in local or global coordinates
:type useLocalCoordinates: boolean
The resulting object has a point on the stack for each object on the original stack.
Vertices and points remain a point. Faces, Wires, Solids, Edges, and Shells are converted
to a point by using their center of mass.
If the stack has zero length, a single point is returned, which is the center of the current
workplane/coordinate system
"""
#convert stack to a list of points
pnts = []
if len(self.objects) == 0:
#nothing on the stack. here, we'll assume we should operate with the
#origin as the context point
pnts.append(self.plane.origin)
else:
for v in self.objects:
pnts.append(v.Center())
return self.newObject(pnts).each(callbackFunction,useLocalCoordinates )
#make a rectangle
def rect(self,xLen,yLen,centered=True,forConstruction=False):
"""
Make a rectangle for each item on the stack.
:param xLen: length in xDirection ( in workplane coordinates )
:type xLen: float > 0
:param yLen: length in yDirection ( in workplane coordinates )
:type yLen: float > 0
:param boolean centered: true if the rect is centered on the reference point, false if the lower-left is on the reference point
:param forConstruction: should the new wires be reference geometry only?
:type forConstruction: true if the wires are for reference, false if they are creating part geometry
:return: a new CQ object with the created wires on the stack
A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
Creates 4 circles at the corners of a square centered on the origin.
Future Enhancements:
better way to handle forConstruction
project points not in the workplane plane onto the workplane plane
"""
def makeRectangleWire(pnt):
#here pnt is in local coordinates due to useLocalCoords=True
(xc,yc,zc) = pnt.toTuple()
if centered:
p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0,0) )
p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0,0) )
p3 = pnt.add(Vector(xLen/2.0, yLen/2.0,0) )
p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0,0) )
else:
p1 = pnt
p2 = pnt.add(Vector(xLen,0,0))
p3 = pnt.add(Vector( xLen,yLen,0 ))
p4 = pnt.add(Vector(0,yLen,0))
w = Wire.makePolygon([p1,p2,p3,p4,p1],forConstruction)
return w
#return Part.makePolygon([p1,p2,p3,p4,p1])
return self.eachpoint(makeRectangleWire,True)
#circle from current point
def circle(self,radius,forConstruction=False):
"""
Make a circle for each item on the stack.
:param radius: radius of the circle
:type radius: float > 0
:param forConstruction: should the new wires be reference geometry only?
:type forConstruction: true if the wires are for reference, false if they are creating part geometry
:return: a new CQ object with the created wires on the stack
A common use case is to use a for-construction rectangle to define the centers of a hole pattern::
s = Workplane().rect(4.0,4.0,forConstruction=True).vertices().circle(0.25)
Creates 4 circles at the corners of a square centered on the origin. Another common case is to use
successive circle() calls to create concentric circles. This works because the center of a circle
is its reference point::
s = Workplane().circle(2.0).circle(1.0)
Creates two concentric circles, which when extruded will form a ring.
Future Enhancements:
better way to handle forConstruction
project points not in the workplane plane onto the workplane plane
"""
def makeCircleWire(obj):
cir = Wire.makeCircle(radius,obj,Vector(0,0,1))
cir.forConstruction = forConstruction
return cir
return self.eachpoint(makeCircleWire,useLocalCoordinates=True)
def polygon(self,nSides,diameter):
"""
Creates a polygon incribed in a circle of the specified diamter for each point on the stack
The first vertex is always oriented in the x direction.
:param nSides: number of sides, must be > 3
:param diameter: the size of the circle the polygon is incribed into
:return: a polygon wire
"""
def _makePolygon(center):
#pnt is a vector in local coordinates
angle = 2.0 *math.pi / nSides
pnts = []
for i in range(nSides+1):
pnts.append( center + Vector((diameter / 2.0 * math.cos(angle*i)),(diameter / 2.0 * math.sin(angle*i)),0))
return Wire.makePolygon(pnts)
return self.eachpoint(_makePolygon,True)
def polyline(self,listOfXYTuple,forConstruction=False):
"""
Create a polyline from a list of points
:param listOfXYTuple: a list of points in Workplane coordinates
:type listOfXYTuple: list of 2-tuples
:param forConstruction: should the new wire be reference geometry only?
:type forConstruction: true if the wire is for reference, false if they are creating part geometry
:return: a new CQ object with the new wire on the stack
*NOTE* most commonly, the resulting wire should be closed.
Future Enhacement:
This should probably yield a list of edges, not a wire, so that
it is possible to combine a polyline with other edges and arcs
"""
vecs = [self.plane.toWorldCoords(p) for p in listOfXYTuple ]
w = Wire.makePolygon(vecs)
if not forConstruction:
self._addPendingWire(w)
return self.newObject([w])
#finish a set of lines.
#
def close(self):
"""
End 2-d construction, and attempt to build a closed wire.
:return: a CQ object with a completed wire on the stack, if possible.
After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary
to convert the edges produced by these into one or more wires.
When a set of edges is closed, cadQuery assumes it is safe to build the group of edges
into a wire. This example builds a simple triangular prism::
s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2)
"""
self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y)
return self.wire()
def largestDimension(self):
"""
Finds the largest dimension in the stack.
Used internally to create thru features, this is how you can compute
how long or wide a feature must be to make sure to cut through all of the material
:return:
"""
#TODO: this implementation is naive and returns the dims of the first solid... most of
#the time this works. but a stronger implementation would be to search all solids.
s = self.findSolid()
if s:
return s.BoundingBox().DiagonalLength * 5.0
else:
return 1000000
def cutEach(self,fcn,useLocalCoords=False):
"""
Evaluates the provided function at each point on the stack ( ie, eachpoint )
and then cuts the result from the context solid.
:param function: a function suitable for use in the eachpoint method: ie, that accepts a vector
:param useLocalCoords: same as for :py:meth:`eachpoint`
:return: a CQ object that contains the resulting solid
:raises: an error if there is not a context solid to cut from
"""
ctxSolid = self.findSolid()
if ctxSolid is None:
raise ValueError ("Must have a solid in the chain to cut from!")
#will contain all of the counterbores as a single compound
results = self.eachpoint(fcn,useLocalCoords).vals()
s = ctxSolid
for cb in results:
s = s.cut(cb)
ctxSolid.wrapped = s.wrapped
return self.newObject([s])
#but parameter list is different so a simple function pointer wont work
def cboreHole(self,diameter,cboreDiameter,cboreDepth,depth=None):
"""
Makes a counterbored hole for each item on the stack.
:param diameter: the diameter of the hole
:type diamter: float > 0
:param cboreDiameter: the diameter of the cbore
:type cboreDiameter: float > 0 and > diameter
:param cboreDepth: depth of the counterbore
:type cboreDepth: float > 0
:param depth: the depth of the hole
:type depth: float > 0 or None to drill thru the entire part.
The surface of the hole is at the current workplane plane.
One hole is created for each item on the stack. A very common use case is to use a
construction rectangle to define the centers of a set of holes, like so::
s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
.vertices().cboreHole(0.125, 0.25,0.125,depth=None)
This sample creates a plate with a set of holes at the corners.
**Plugin Note**: this is one example of the power of plugins. Counterbored holes are quite time consuming
to create, but are quite easily defined by users.
see :py:meth:`cskHole` to make countersinks instead of counterbores
"""
if depth is None:
depth = self.largestDimension()
def _makeCbore(center):
"""
Makes a single hole with counterbore at the supplied point
returns a solid suitable for subtraction
pnt is in local coordinates
"""
boreDir = Vector(0,0,-1)
#first make the hole
hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
#add the counter bore
cbore = Solid.makeCylinder(cboreDiameter/2.0,cboreDepth,center,boreDir)
r = hole.fuse(cbore)
return r
return self.cutEach(_makeCbore,True)
#TODO: almost all code duplicated!
#but parameter list is different so a simple function pointer wont work
def cskHole(self,diameter, cskDiameter,cskAngle,depth=None):
"""
Makes a countersunk hole for each item on the stack.
:param diameter: the diameter of the hole
:type diamter: float > 0
:param cskDiameter: the diameter of the countersink
:type cskDiameter: float > 0 and > diameter
:param cskAngle: angle of the countersink, in degrees ( 82 is common )
:type cskAngle: float > 0
:param depth: the depth of the hole
:type depth: float > 0 or None to drill thru the entire part.
The surface of the hole is at the current workplane.
One hole is created for each item on the stack. A very common use case is to use a
construction rectangle to define the centers of a set of holes, like so::
s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
.vertices().cskHole(0.125, 0.25,82,depth=None)
This sample creates a plate with a set of holes at the corners.
**Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
to create, but are quite easily defined by users.
see :py:meth:`cboreHole` to make counterbores instead of countersinks
"""
if depth is None:
depth = self.largestDimension()
def _makeCsk(center):
#center is in local coordinates
boreDir = Vector(0,0,-1)
#first make the hole
hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coords!
r = cskDiameter / 2.0
h = r / math.tan(math.radians(cskAngle / 2.0))
csk = Solid.makeCone(r,0.0,h,center,boreDir)
r = hole.fuse(csk)
return r
return self.cutEach(_makeCsk,True)
#TODO: almost all code duplicated!
#but parameter list is different so a simple function pointer wont work
def hole(self,diameter,depth=None):
"""
Makes a hole for each item on the stack.
:param diameter: the diameter of the hole
:type diamter: float > 0
:param depth: the depth of the hole
:type depth: float > 0 or None to drill thru the entire part.
The surface of the hole is at the current workplane.
One hole is created for each item on the stack. A very common use case is to use a
construction rectangle to define the centers of a set of holes, like so::
s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\
.vertices().hole(0.125, 0.25,82,depth=None)
This sample creates a plate with a set of holes at the corners.
**Plugin Note**: this is one example of the power of plugins. CounterSunk holes are quite time consuming
to create, but are quite easily defined by users.
see :py:meth:`cboreHole` and :py:meth:`cskHole` to make counterbores or countersinks
"""
if depth is None:
depth = self.largestDimension()
def _makeHole(center):
"""
Makes a single hole with counterbore at the supplied point
returns a solid suitable for subtraction
pnt is in local coordinates
"""
boreDir = Vector(0,0,-1)
#first make the hole
hole = Solid.makeCylinder(diameter/2.0,depth,center,boreDir) # local coordianates!
return hole
return self.cutEach(_makeHole,True)
#TODO: duplicated code with _extrude and extrude
def twistExtrude(self,distance,angleDegrees,combine=True):
"""
Extrudes a wire in the direction normal to the plane, but also twists by the specified angle over the
length of the extrusion
The center point of the rotation will be the center of the workplane
See extrude for more details, since this method is the same except for the the addition of the angle.
in fact, if angle=0, the result is the same as a linear extrude.
**NOTE** This method can create complex calculations, so be careful using it with complex geometries
:param distance: the distance to extrude normal to the workplane
:param angle: angline ( in degrees) to rotate through the extrusion
:param boolean combine: True to combine the resulting solid with parent solids if found.
:return: a CQ object with the resulting solid selected.
"""
#group wires together into faces based on which ones are inside the others
#result is a list of lists
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires),self.plane,[])
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
#compute extrusion vector and extrude
eDir = self.plane.zDir.multiply(distance)
#one would think that fusing faces into a compound and then extruding would work,
#but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
#but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work
#underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
r = None
for ws in wireSets:
thisObj = Solid.extrudeLinearWithRotation(ws[0],ws[1:],self.plane.origin, eDir,angleDegrees)
if r is None:
r = thisObj
else:
r = r.fuse(thisObj)
if combine:
return self._combineWithBase(r)
else:
return self.newObject([r])
def extrude(self,distance,combine=True):
"""
Use all un-extruded wires in the parent chain to create a prismatic solid.
:param distance: the distance to extrude, normal to the workplane plane
:type distance: float, negative means opposite the normal direction
:param boolean combine: True to combine the resulting solid with parent solids if found.
:return: a CQ object with the resulting solid selected.
extrude always *adds* material to a part.
The returned object is always a CQ object, and depends on wither combine is True, and
whether a context solid is already defined:
* if combine is False, the new value is pushed onto the stack.
* if combine is true, the value is combined with the context solid if it exists,
and the resulting solid becomes the new context solid.
FutureEnhancement:
Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane
extrude to surface. this is quite tricky since the surface selected may not be planar
"""
r = self._extrude(distance) #returns a Solid ( or a compound if there were multiple )
if combine:
return self._combineWithBase(r)
else:
return self.newObject([r])
def _combineWithBase2(self,obj):
"""
Combines the provided object with the base solid, if one can be found.
:param obj:
:return: a new object that represents the result of combining the base object with obj,
or obj if one could not be found
"""
baseSolid = self.findSolid(searchParents=True)
r = obj
if baseSolid is not None:
r = baseSolid.fuse(obj)
baseSolid.wrapped = r.wrapped
return self.newObject([r])
def _combineWithBase(self,obj):
"""
Combines the provided object with the base solid, if one can be found.
:param obj:
:return: a new object that represents the result of combining the base object with obj,
or obj if one could not be found
"""
baseSolid = self.findSolid(searchParents=True)
r = obj
if baseSolid is not None:
r = baseSolid.fuse(obj)
baseSolid.wrapped = r.wrapped
return self.newObject([r])
def combine(self):
"""
Attempts to combine all of the items on the items on the stack into a single item.
WARNING: all of the items must be of the same type!
:raises: ValueError if there are no items on the stack, or if they cannot be combined
:return: a CQ object with the resulting object selected
"""
items = list(self.objects)
s = items.pop(0)
for ss in items:
s = s.fuse(ss)
return self.newObject([s])
def union(self,toUnion=None,combine=True):
"""
Unions all of the items on the stack of toUnion with the current solid.
If there is no current solid, the items in toUnion are unioned together.
if combine=True, the result and the original are updated to point to the new object
if combine=False, the result will be on the stack, but the original is unmodified
:param toUnion:
:type toUnion: a solid object, or a CQ object having a solid,
:raises: ValueError if there is no solid to add to in the chain
:return: a CQ object with the resulting object selected
"""
#first collect all of the items together
if type(toUnion) == CQ or type(toUnion) == Workplane:
solids = toUnion.solids().vals()
if len(solids) < 1 :
raise ValueError("CQ object must have at least one solid on the stack to union!")
newS = solids.pop(0)
for s in solids:
newS = newS.fuse(s)
elif type(toUnion) == Solid:
newS = toUnion
else:
raise ValueError("Cannot union Type '%s' " % str(type(toUnion)))
#now combine with existing solid, if there is one
solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from
if combine and solidRef is not None:
t = solidRef.fuse(newS)
solidRef.wrapped = newS.wrapped
return self.newObject([t])
else:
return self.newObject([newS])
def cut(self,toCut,combine=True):
"""
Cuts the provided solid from the current solid, IE, perform a solid subtraction
if combine=True, the result and the original are updated to point to the new object
if combine=False, the result will be on the stack, but the original is unmodified
:param toCut: object to cut
:type toCut: a solid object, or a CQ object having a solid,
:raises: ValueError if there is no solid to subtract from in the chain
:return: a CQ object with the resulting object selected
"""
solidRef = self.findSolid(searchStack=True,searchParents=True) #look for parents to cut from
if solidRef is None:
raise ValueError("Cannot find solid to cut from!!!")
solidToCut = None
if type(toCut) == CQ or type(toCut) == Workplane:
solidToCut = toCut.val()
elif type(toCut) == Solid:
solidToCut = toCut
else:
raise ValueError("Cannot cut Type '%s' " % str(type(toCut)))
newS = solidRef.cut(solidToCut)
if combine:
solidRef.wrapped = newS.wrapped
return self.newObject([newS])
def cutBlind(self,distanceToCut):
"""
Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
Similar to extrude, except that a solid in the parent chain is required to remove material from.
cutBlind always removes material from a part.
:param distanceToCut: distance to extrude before cutting
:type distanceToCut: float, >0 means in the positive direction of the workplane normal, <0 means in the negative direction
:raises: ValueError if there is no solid to subtract from in the chain
:return: a CQ object with the resulting object selected
see :py:meth:`cutThruAll` to cut material from the entire part
Future Enhancements:
Cut Up to Surface
"""
#first, make the object
toCut = self._extrude(distanceToCut)
#now find a solid in the chain
solidRef = self.findSolid()
s= solidRef.cut(toCut)
solidRef.wrapped = s.wrapped
return self.newObject([s])
def cutThruAll(self,positive=False):
"""
Use all un-extruded wires in the parent chain to create a prismatic cut from existing solid.
Similar to extrude, except that a solid in the parent chain is required to remove material from.
cutThruAll always removes material from a part.
:param boolean positive: True to cut in the positive direction, false to cut in the negative direction
:raises: ValueError if there is no solid to subtract from in the chain
:return: a CQ object with the resulting object selected
see :py:meth:`cutBlind` to cut material to a limited depth
"""
maxDim = self.largestDimension()
if not positive:
maxDim *= (-1.0)
return self.cutBlind(maxDim)
def loft(self,filled=True,combine=True):
"""
Make a lofted solid, through the set of wires.
:return:
"""
wiresToLoft = self.ctx.pendingWires
self.ctx.pendingWires = []
r = Solid.makeLoft(wiresToLoft)
if combine:
parentSolid = self.findSolid(searchStack=False,searchParents=True)
if parentSolid is not None:
r = parentSolid.fuse(r)
parentSolid.wrapped = r.wrapped
return self.newObject([r])
def _extrude(self,distance):
"""
Make a prismatic solid from the existing set of pending wires.
:param distance: distance to extrude
:return: a FreeCAD solid, suitable for boolean operations.
This method is a utility method, primarily for plugin and internal use.
It is the basis for cutBlind,extrude,cutThruAll, and all similar methods.
Future Enhancements:
extrude along a profile ( sweep )
"""
#group wires together into faces based on which ones are inside the others
#result is a list of lists
s = time.time()
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires),self.plane,[])
#print "sorted wires in %d sec" % ( time.time() - s )
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
#compute extrusion vector and extrude
eDir = self.plane.zDir.multiply(distance)
#one would think that fusing faces into a compound and then extruding would work,
#but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
#but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work
#underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are multiple sets
# IMPORTANT NOTE: OCC is slow slow slow in boolean operations. So you do NOT want to fuse each item to
# another and save the result-- instead, you want to combine all of the new items into a compound, and fuse
# them together!!!
"""
r = None
for ws in wireSets:
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
if r is None:
r = thisObj
else:
s = time.time()
r = r.fuse(thisObj)
print "Fused in %0.3f sec" % ( time.time() - s )
return r
"""
toFuse = []
for ws in wireSets:
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
toFuse.append(thisObj)
return Compound.makeCompound(toFuse)
def box(self,length,width,height,centered=(True,True,True),combine=True):
"""
Return a 3d box with specified dimensions for each object on the stack.
:param length: box size in X direction
:type length: float > 0
:param width: box size in Y direction
:type width: float > 0
:param height: box size in Z direction
:type height: float > 0
:param centered: should the box be centered, or should reference point be at the lower bound of the range?
:param combine: should the results be combined with other solids on the stack ( and each other)?
:type combine: true to combine shapes, false otherwise.
Centered is a tuple that describes whether the box should be centered on the x,y, and z axes. If true,
the box is centered on the respective axis relative to the workplane origin, if false, the workplane center
will represent the lower bound of the resulting box
one box is created for each item on the current stack. If no items are on the stack, one box using
the current workplane center is created.
If combine is true, the result will be a single object on the stack:
if a solid was found in the chain, the result is that solid with all boxes produced fused onto it
otherwise, the result is the combination of all the produced boxes
if combine is false, the result will be a list of the boxes produced
Most often boxes form the basis for a part::
#make a single box with lower left corner at origin
s = Workplane().box(1,2,3,centered=(False,False,False)
But sometimes it is useful to create an array of them:
#create 4 small square bumps on a larger base plate:
s = Workplane().box(4,4,0.5).faces(">Z").workplane()\
.rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True)
"""
def _makebox(pnt):
#(xp,yp,zp) = self.plane.toLocalCoords(pnt)
(xp,yp,zp) = pnt.toTuple()
if centered[0]:
xp = xp-(length/2.0)
if centered[1]:
yp = yp-(width/2.0)
if centered[2]:
zp = zp-(height/2.0)
return Solid.makeBox(length,width,height,Vector(xp,yp,zp))
boxes = self.eachpoint(_makebox,True)
#if combination is not desired, just return the created boxes
if not combine:
return boxes
else:
#combine everything
return self.union(boxes)