First attempt at python2 and python3 support in single codebase

4 tests failing on python3 (CQGI, AMF export)
This commit is contained in:
Adam Urbanczyk
2017-09-17 00:57:12 +02:00
parent 231b691b1b
commit 1e05a45f9c
22 changed files with 2068 additions and 1771 deletions

View File

@ -1,21 +1,21 @@
#these items point to the OCC implementation # these items point to the OCC implementation
from .occ_impl.geom import Plane,BoundBox,Vector,Matrix from .occ_impl.geom import Plane, BoundBox, Vector, Matrix
from .occ_impl.shapes import Shape,Vertex,Edge,Face,Wire,Solid,Shell,Compound,sortWiresByBuildOrder from .occ_impl.shapes import Shape, Vertex, Edge, Face, Wire, Solid, Shell, Compound, sortWiresByBuildOrder
from .occ_impl import exporters from .occ_impl import exporters
from .occ_impl import importers from .occ_impl import importers
#these items are the common implementation # these items are the common implementation
#the order of these matter # the order of these matter
from .selectors import * from .selectors import *
from .cq import * from .cq import *
__all__ = [ __all__ = [
'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder', 'CQ', 'Workplane', 'plugins', 'selectors', 'Plane', 'BoundBox', 'Matrix', 'Vector', 'sortWiresByBuildOrder',
'Shape','Vertex','Edge','Wire','Face','Solid','Shell','Compound','exporters', 'importers', 'Shape', 'Vertex', 'Edge', 'Wire', 'Face', 'Solid', 'Shell', 'Compound', 'exporters', 'importers',
'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector', 'NearestToPointSelector', 'ParallelDirSelector', 'DirectionSelector', 'PerpendicularDirSelector',
'TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins' 'TypeSelector', 'DirectionMinMaxSelector', 'StringSyntaxSelector', 'Selector', 'plugins'
] ]
__version__ = "1.0.0" __version__ = "1.0.0"

View File

@ -31,9 +31,11 @@ class CQContext(object):
All objects in the same CQ chain share a reference to this same object instance All objects in the same CQ chain share a reference to this same object instance
which allows for shared state when needed, which allows for shared state when needed,
""" """
def __init__(self): def __init__(self):
self.pendingWires = [] # a list of wires that have been created and need to be extruded self.pendingWires = [] # a list of wires that have been created and need to be extruded
self.pendingEdges = [] # a list of created pending edges that need to be joined into wires # a list of created pending edges that need to be joined into wires
self.pendingEdges = []
# a reference to the first point for a set of edges. # a reference to the first point for a set of edges.
# Used to determine how to behave when close() is called # Used to determine how to behave when close() is called
self.firstPoint = None self.firstPoint = None
@ -167,8 +169,8 @@ class CQ(object):
Most of the time, both objects will contain a single solid, which is Most of the time, both objects will contain a single solid, which is
combined and returned on the stack of the new object. combined and returned on the stack of the new object.
""" """
#loop through current stack objects, and combine them # loop through current stack objects, and combine them
#TODO: combine other types of objects as well, like edges and wires # TODO: combine other types of objects as well, like edges and wires
toCombine = self.solids().vals() toCombine = self.solids().vals()
if otherCQToCombine: if otherCQToCombine:
@ -178,13 +180,13 @@ class CQ(object):
if len(toCombine) < 1: if len(toCombine) < 1:
raise ValueError("Cannot Combine: at least one solid required!") raise ValueError("Cannot Combine: at least one solid required!")
#get context solid and we don't want to find our own objects # get context solid and we don't want to find our own objects
ctxSolid = self.findSolid(searchStack=False, searchParents=True) ctxSolid = self.findSolid(searchStack=False, searchParents=True)
if ctxSolid is None: if ctxSolid is None:
ctxSolid = toCombine.pop(0) ctxSolid = toCombine.pop(0)
#now combine them all. make sure to save a reference to the ctxSolid pointer! # now combine them all. make sure to save a reference to the ctxSolid pointer!
s = ctxSolid s = ctxSolid
for tc in toCombine: for tc in toCombine:
s = s.fuse(tc) s = s.fuse(tc)
@ -311,9 +313,9 @@ class CQ(object):
n1 = f1.normalAt() n1 = f1.normalAt()
# test normals (direction of planes) # test normals (direction of planes)
if not ((abs(n0.x-n1.x) < self.ctx.tolerance) or if not ((abs(n0.x - n1.x) < self.ctx.tolerance) or
(abs(n0.y-n1.y) < self.ctx.tolerance) or (abs(n0.y - n1.y) < self.ctx.tolerance) or
(abs(n0.z-n1.z) < self.ctx.tolerance)): (abs(n0.z - n1.z) < self.ctx.tolerance)):
return False return False
# test if p1 is on the plane of f0 (offset of planes) # test if p1 is on the plane of f0 (offset of planes)
@ -328,23 +330,24 @@ class CQ(object):
""" """
xd = Vector(0, 0, 1).cross(normal) xd = Vector(0, 0, 1).cross(normal)
if xd.Length < self.ctx.tolerance: if xd.Length < self.ctx.tolerance:
#this face is parallel with the x-y plane, so choose x to be in global coordinates # this face is parallel with the x-y plane, so choose x to be in global coordinates
xd = Vector(1, 0, 0) xd = Vector(1, 0, 0)
return xd return xd
if len(self.objects) > 1: if len(self.objects) > 1:
# are all objects 'PLANE'? # are all objects 'PLANE'?
if not all(o.geomType() in ('PLANE','CIRCLE') for o in self.objects): if not all(o.geomType() in ('PLANE', 'CIRCLE') for o in self.objects):
raise ValueError("If multiple objects selected, they all must be planar faces.") raise ValueError(
"If multiple objects selected, they all must be planar faces.")
# are all faces co-planar with each other? # are all faces co-planar with each other?
if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]): if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]):
raise ValueError("Selected faces must be co-planar.") raise ValueError("Selected faces must be co-planar.")
if centerOption == 'CenterOfMass': if centerOption == 'CenterOfMass':
center = Shape.CombinedCenter(self.objects) center = Shape.CombinedCenter(self.objects)
elif centerOption == 'CenterOfBoundBox': elif centerOption == 'CenterOfBoundBox':
center = Shape.CombinedCenterOfBoundBox(self.objects) center = Shape.CombinedCenterOfBoundBox(self.objects)
normal = self.objects[0].normalAt() normal = self.objects[0].normalAt()
xDir = _computeXdir(normal) xDir = _computeXdir(normal)
@ -353,38 +356,39 @@ class CQ(object):
obj = self.objects[0] obj = self.objects[0]
if isinstance(obj, Face): if isinstance(obj, Face):
if centerOption == 'CenterOfMass': if centerOption == 'CenterOfMass':
center = obj.Center() center = obj.Center()
elif centerOption == 'CenterOfBoundBox': elif centerOption == 'CenterOfBoundBox':
center = obj.CenterOfBoundBox() center = obj.CenterOfBoundBox()
normal = obj.normalAt(center) normal = obj.normalAt(center)
xDir = _computeXdir(normal) xDir = _computeXdir(normal)
else: else:
if hasattr(obj, 'Center'): if hasattr(obj, 'Center'):
if centerOption == 'CenterOfMass': if centerOption == 'CenterOfMass':
center = obj.Center() center = obj.Center()
elif centerOption == 'CenterOfBoundBox': elif centerOption == 'CenterOfBoundBox':
center = obj.CenterOfBoundBox() center = obj.CenterOfBoundBox()
normal = self.plane.zDir normal = self.plane.zDir
xDir = self.plane.xDir xDir = self.plane.xDir
else: else:
raise ValueError("Needs a face or a vertex or point on a work plane") raise ValueError(
"Needs a face or a vertex or point on a work plane")
#invert if requested # invert if requested
if invert: if invert:
normal = normal.multiply(-1.0) normal = normal.multiply(-1.0)
#offset origin if desired # offset origin if desired
offsetVector = normal.normalized().multiply(offset) offsetVector = normal.normalized().multiply(offset)
offsetCenter = center.add(offsetVector) offsetCenter = center.add(offsetVector)
#make the new workplane # make the new workplane
plane = Plane(offsetCenter, xDir, normal) plane = Plane(offsetCenter, xDir, normal)
s = Workplane(plane) s = Workplane(plane)
s.parent = self s.parent = self
s.ctx = self.ctx s.ctx = self.ctx
#a new workplane has the center of the workplane on the stack # a new workplane has the center of the workplane on the stack
return s return s
def first(self): def first(self):
@ -479,7 +483,7 @@ class CQ(object):
toReturn = self._collectProperty(objType) toReturn = self._collectProperty(objType)
if selector is not None: if selector is not None:
if isinstance(selector, str) or isinstance(selector, unicode): if isinstance(selector, str) or isinstance(selector, str):
selectorObj = selectors.StringSyntaxSelector(selector) selectorObj = selectors.StringSyntaxSelector(selector)
else: else:
selectorObj = selector selectorObj = selector
@ -716,7 +720,7 @@ class CQ(object):
one object, but is not cool for multiple. one object, but is not cool for multiple.
""" """
#center point is the first point in the vector # center point is the first point in the vector
endVec = Vector(axisEndPoint) endVec = Vector(axisEndPoint)
def _rot(obj): def _rot(obj):
@ -743,17 +747,17 @@ class CQ(object):
for o in self.objects]) for o in self.objects])
def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)): def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)):
""" """
Mirror a single CQ object. This operation is the same as in the FreeCAD PartWB's mirroring Mirror a single CQ object. This operation is the same as in the FreeCAD PartWB's mirroring
:param mirrorPlane: the plane to mirror about
:type mirrorPlane: string, one of "XY", "YX", "XZ", "ZX", "YZ", "ZY" the planes
:param basePointVector: the base point to mirror about
:type basePointVector: tuple
"""
newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)])
return newS.first()
:param mirrorPlane: the plane to mirror about
:type mirrorPlane: string, one of "XY", "YX", "XZ", "ZX", "YZ", "ZY" the planes
:param basePointVector: the base point to mirror about
:type basePointVector: tuple
"""
newS = self.newObject(
[self.objects[0].mirror(mirrorPlane, basePointVector)])
return newS.first()
def translate(self, vec): def translate(self, vec):
""" """
@ -765,7 +769,6 @@ class CQ(object):
""" """
return self.newObject([o.translate(vec) for o in self.objects]) return self.newObject([o.translate(vec) for o in self.objects])
def shell(self, thickness): def shell(self, thickness):
""" """
Remove the selected faces to create a shell of the specified thickness. Remove the selected faces to create a shell of the specified thickness.
@ -935,7 +938,7 @@ class Workplane(CQ):
if inPlane.__class__.__name__ == 'Plane': if inPlane.__class__.__name__ == 'Plane':
tmpPlane = inPlane tmpPlane = inPlane
elif isinstance(inPlane, str) or isinstance(inPlane, unicode): elif isinstance(inPlane, str) or isinstance(inPlane, str):
tmpPlane = Plane.named(inPlane, origin) tmpPlane = Plane.named(inPlane, origin)
else: else:
tmpPlane = None tmpPlane = None
@ -964,7 +967,7 @@ class Workplane(CQ):
:return: a new work plane, transformed as requested :return: a new work plane, transformed as requested
""" """
#old api accepted a vector, so we'll check for that. # old api accepted a vector, so we'll check for that.
if rotate.__class__.__name__ == 'Vector': if rotate.__class__.__name__ == 'Vector':
rotate = rotate.toTuple() rotate = rotate.toTuple()
@ -990,7 +993,7 @@ class Workplane(CQ):
:return: a new Workplane object with the current workplane as a parent. :return: a new Workplane object with the current workplane as a parent.
""" """
#copy the current state to the new object # copy the current state to the new object
ns = Workplane("XY") ns = Workplane("XY")
ns.plane = self.plane ns.plane = self.plane
ns.parent = self ns.parent = self
@ -1026,7 +1029,8 @@ class Workplane(CQ):
elif isinstance(obj, Vector): elif isinstance(obj, Vector):
p = obj p = obj
else: else:
raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) raise RuntimeError(
"Cannot convert object type '%s' to vector " % type(obj))
if useLocalCoords: if useLocalCoords:
return self.plane.toLocalCoords(p) return self.plane.toLocalCoords(p)
@ -1055,10 +1059,10 @@ class Workplane(CQ):
for y in range(yCount): for y in range(yCount):
lpoints.append((xSpacing * x, ySpacing * y)) lpoints.append((xSpacing * x, ySpacing * y))
#shift points down and left relative to origin if requested # shift points down and left relative to origin if requested
if center: if center:
xc = xSpacing*(xCount-1) * 0.5 xc = xSpacing * (xCount - 1) * 0.5
yc = ySpacing*(yCount-1) * 0.5 yc = ySpacing * (yCount - 1) * 0.5
cpoints = [] cpoints = []
for p in lpoints: for p in lpoints:
cpoints.append((p[0] - xc, p[1] - yc)) cpoints.append((p[0] - xc, p[1] - yc))
@ -1204,7 +1208,7 @@ class Workplane(CQ):
p = self._findFromPoint(True) p = self._findFromPoint(True)
return self.lineTo(xCoord, p.y, forConstruction) return self.lineTo(xCoord, p.y, forConstruction)
#absolute move in current plane, not drawing # absolute move in current plane, not drawing
def moveTo(self, x=0, y=0): def moveTo(self, x=0, y=0):
""" """
Move to the specified point, without drawing. Move to the specified point, without drawing.
@ -1223,7 +1227,7 @@ class Workplane(CQ):
newCenter = Vector(x, y, 0) newCenter = Vector(x, y, 0)
return self.newObject([self.plane.toWorldCoords(newCenter)]) return self.newObject([self.plane.toWorldCoords(newCenter)])
#relative move in current plane, not drawing # relative move in current plane, not drawing
def move(self, xDist=0, yDist=0): def move(self, xDist=0, yDist=0):
""" """
Move the specified distance from the current point, without drawing. Move the specified distance from the current point, without drawing.
@ -1334,19 +1338,20 @@ class Workplane(CQ):
faster implementation: this one transforms 3 times to accomplish the result faster implementation: this one transforms 3 times to accomplish the result
""" """
#convert edges to a wire, if there are pending edges # convert edges to a wire, if there are pending edges
n = self.wire(forConstruction=False) n = self.wire(forConstruction=False)
#attempt to consolidate wires together. # attempt to consolidate wires together.
consolidated = n.consolidateWires() consolidated = n.consolidateWires()
rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix) rotatedWires = self.plane.rotateShapes(
consolidated.wires().vals(), matrix)
for w in rotatedWires: for w in rotatedWires:
consolidated.objects.append(w) consolidated.objects.append(w)
consolidated._addPendingWire(w) consolidated._addPendingWire(w)
#attempt again to consolidate all of the wires # attempt again to consolidate all of the wires
c = consolidated.consolidateWires() c = consolidated.consolidateWires()
return c return c
@ -1369,10 +1374,10 @@ class Workplane(CQ):
Future Enhancements: Future Enhancements:
mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
""" """
#convert edges to a wire, if there are pending edges # convert edges to a wire, if there are pending edges
n = self.wire(forConstruction=False) n = self.wire(forConstruction=False)
#attempt to consolidate wires together. # attempt to consolidate wires together.
consolidated = n.consolidateWires() consolidated = n.consolidateWires()
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(),
@ -1382,7 +1387,7 @@ class Workplane(CQ):
consolidated.objects.append(w) consolidated.objects.append(w)
consolidated._addPendingWire(w) consolidated._addPendingWire(w)
#attempt again to consolidate all of the wires # attempt again to consolidate all of the wires
return consolidated.consolidateWires() return consolidated.consolidateWires()
def mirrorX(self): def mirrorX(self):
@ -1399,10 +1404,10 @@ class Workplane(CQ):
Future Enhancements: Future Enhancements:
mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness
""" """
#convert edges to a wire, if there are pending edges # convert edges to a wire, if there are pending edges
n = self.wire(forConstruction=False) n = self.wire(forConstruction=False)
#attempt to consolidate wires together. # attempt to consolidate wires together.
consolidated = n.consolidateWires() consolidated = n.consolidateWires()
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(),
@ -1412,7 +1417,7 @@ class Workplane(CQ):
consolidated.objects.append(w) consolidated.objects.append(w)
consolidated._addPendingWire(w) consolidated._addPendingWire(w)
#attempt again to consolidate all of the wires # attempt again to consolidate all of the wires
return consolidated.consolidateWires() return consolidated.consolidateWires()
def _addPendingEdge(self, edge): def _addPendingEdge(self, edge):
@ -1458,15 +1463,15 @@ class Workplane(CQ):
if len(wires) < 2: if len(wires) < 2:
return self return self
#TODO: this makes the assumption that either all wires could be combined, or none. # 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 # in reality trying each combination of wires is probably not reasonable anyway
w = Wire.combine(wires) w = Wire.combine(wires)
#ok this is a little tricky. if we consolidate wires, we have to actually # 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 # modify the pendingWires collection to remove the original ones, and replace them
#with the consolidate done # with the consolidate done
#since we are already assuming that all wires could be consolidated, its easy, we just # since we are already assuming that all wires could be consolidated, its easy, we just
#clear the pending wire list # clear the pending wire list
r = self.newObject([w]) r = self.newObject([w])
r.ctx.pendingWires = [] r.ctx.pendingWires = []
r._addPendingWire(w) r._addPendingWire(w)
@ -1495,7 +1500,7 @@ class Workplane(CQ):
edges = self.ctx.pendingEdges edges = self.ctx.pendingEdges
#do not consolidate if there are no free edges # do not consolidate if there are no free edges
if len(edges) == 0: if len(edges) == 0:
return self return self
@ -1506,7 +1511,6 @@ class Workplane(CQ):
if type(e) != Edge: if type(e) != Edge:
others.append(e) others.append(e)
w = Wire.assembleEdges(edges) w = Wire.assembleEdges(edges)
if not forConstruction: if not forConstruction:
self._addPendingWire(w) self._addPendingWire(w)
@ -1549,7 +1553,7 @@ class Workplane(CQ):
for obj in self.objects: for obj in self.objects:
if useLocalCoordinates: if useLocalCoordinates:
#TODO: this needs to work for all types of objects, not just vectors! # TODO: this needs to work for all types of objects, not just vectors!
r = callBackFunction(self.plane.toLocalCoords(obj)) r = callBackFunction(self.plane.toLocalCoords(obj))
r = r.transformShape(self.plane.rG) r = r.transformShape(self.plane.rG)
else: else:
@ -1580,11 +1584,11 @@ class Workplane(CQ):
If the stack has zero length, a single point is returned, which is the center of the current If the stack has zero length, a single point is returned, which is the center of the current
workplane/coordinate system workplane/coordinate system
""" """
#convert stack to a list of points # convert stack to a list of points
pnts = [] pnts = []
if len(self.objects) == 0: if len(self.objects) == 0:
#nothing on the stack. here, we'll assume we should operate with the # nothing on the stack. here, we'll assume we should operate with the
#origin as the context point # origin as the context point
pnts.append(self.plane.origin) pnts.append(self.plane.origin)
else: else:
@ -1623,10 +1627,10 @@ class Workplane(CQ):
# Here pnt is in local coordinates due to useLocalCoords=True # Here pnt is in local coordinates due to useLocalCoords=True
# (xc,yc,zc) = pnt.toTuple() # (xc,yc,zc) = pnt.toTuple()
if centered: if centered:
p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0, 0)) p1 = pnt.add(Vector(xLen / -2.0, yLen / -2.0, 0))
p2 = 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)) p3 = pnt.add(Vector(xLen / 2.0, yLen / 2.0, 0))
p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0, 0)) p4 = pnt.add(Vector(xLen / -2.0, yLen / 2.0, 0))
else: else:
p1 = pnt p1 = pnt
p2 = pnt.add(Vector(xLen, 0, 0)) p2 = pnt.add(Vector(xLen, 0, 0))
@ -1635,11 +1639,11 @@ class Workplane(CQ):
w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction) w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction)
return w return w
#return Part.makePolygon([p1,p2,p3,p4,p1]) # return Part.makePolygon([p1,p2,p3,p4,p1])
return self.eachpoint(makeRectangleWire, True) return self.eachpoint(makeRectangleWire, True)
#circle from current point # circle from current point
def circle(self, radius, forConstruction=False): def circle(self, radius, forConstruction=False):
""" """
Make a circle for each item on the stack. Make a circle for each item on the stack.
@ -1688,12 +1692,12 @@ class Workplane(CQ):
:return: a polygon wire :return: a polygon wire
""" """
def _makePolygon(center): def _makePolygon(center):
#pnt is a vector in local coordinates # pnt is a vector in local coordinates
angle = 2.0 * math.pi / nSides angle = 2.0 * math.pi / nSides
pnts = [] pnts = []
for i in range(nSides+1): for i in range(nSides + 1):
pnts.append(center + Vector((diameter / 2.0 * math.cos(angle*i)), pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)),
(diameter / 2.0 * math.sin(angle*i)), 0)) (diameter / 2.0 * math.sin(angle * i)), 0))
return Wire.makePolygon(pnts, forConstruction) return Wire.makePolygon(pnts, forConstruction)
return self.eachpoint(_makePolygon, True) return self.eachpoint(_makePolygon, True)
@ -1749,7 +1753,7 @@ class Workplane(CQ):
self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y) self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y)
# Need to reset the first point after closing a wire # Need to reset the first point after closing a wire
self.ctx.firstPoint=None self.ctx.firstPoint = None
return self.wire() return self.wire()
@ -1760,8 +1764,8 @@ class Workplane(CQ):
how long or wide a feature must be to make sure to cut through all of the material how long or wide a feature must be to make sure to cut through all of the material
:return: A value representing the largest dimension of the first solid on the stack :return: A value representing the largest dimension of the first solid on the stack
""" """
#TODO: this implementation is naive and returns the dims of the first solid... most of # TODO: this implementation is naive and returns the dims of the first solid... most of
#TODO: the time this works. but a stronger implementation would be to search all solids. # TODO: the time this works. but a stronger implementation would be to search all solids.
s = self.findSolid() s = self.findSolid()
if s: if s:
return s.BoundingBox().DiagonalLength * 5.0 return s.BoundingBox().DiagonalLength * 5.0
@ -1782,18 +1786,19 @@ class Workplane(CQ):
if ctxSolid is None: if ctxSolid is None:
raise ValueError("Must have a solid in the chain to cut from!") raise ValueError("Must have a solid in the chain to cut from!")
#will contain all of the counterbores as a single compound # will contain all of the counterbores as a single compound
results = self.eachpoint(fcn, useLocalCoords).vals() results = self.eachpoint(fcn, useLocalCoords).vals()
s = ctxSolid s = ctxSolid
for cb in results: for cb in results:
s = s.cut(cb) s = s.cut(cb)
if clean: s = s.clean() if clean:
s = s.clean()
ctxSolid.wrapped = s.wrapped ctxSolid.wrapped = s.wrapped
return self.newObject([s]) return self.newObject([s])
#but parameter list is different so a simple function pointer wont work # but parameter list is different so a simple function pointer wont work
def cboreHole(self, diameter, cboreDiameter, cboreDepth, depth=None, clean=True): def cboreHole(self, diameter, cboreDiameter, cboreDepth, depth=None, clean=True):
""" """
Makes a counterbored hole for each item on the stack. Makes a counterbored hole for each item on the stack.
@ -1834,18 +1839,20 @@ class Workplane(CQ):
pnt is in local coordinates pnt is in local coordinates
""" """
boreDir = Vector(0, 0, -1) boreDir = Vector(0, 0, -1)
#first make the hole # first make the hole
hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coordianates! hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coordianates!
#add the counter bore # add the counter bore
cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir) cbore = Solid.makeCylinder(
cboreDiameter / 2.0, cboreDepth, center, boreDir)
r = hole.fuse(cbore) r = hole.fuse(cbore)
return r return r
return self.cutEach(_makeCbore, True, clean) return self.cutEach(_makeCbore, True, clean)
#TODO: almost all code duplicated! # TODO: almost all code duplicated!
#but parameter list is different so a simple function pointer wont work # but parameter list is different so a simple function pointer wont work
def cskHole(self, diameter, cskDiameter, cskAngle, depth=None, clean=True): def cskHole(self, diameter, cskDiameter, cskAngle, depth=None, clean=True):
""" """
Makes a countersunk hole for each item on the stack. Makes a countersunk hole for each item on the stack.
@ -1881,12 +1888,13 @@ class Workplane(CQ):
depth = self.largestDimension() depth = self.largestDimension()
def _makeCsk(center): def _makeCsk(center):
#center is in local coordinates # center is in local coordinates
boreDir = Vector(0, 0, -1) boreDir = Vector(0, 0, -1)
#first make the hole # first make the hole
hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coords! hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coords!
r = cskDiameter / 2.0 r = cskDiameter / 2.0
h = r / math.tan(math.radians(cskAngle / 2.0)) h = r / math.tan(math.radians(cskAngle / 2.0))
csk = Solid.makeCone(r, 0.0, h, center, boreDir) csk = Solid.makeCone(r, 0.0, h, center, boreDir)
@ -1895,8 +1903,8 @@ class Workplane(CQ):
return self.cutEach(_makeCsk, True, clean) return self.cutEach(_makeCsk, True, clean)
#TODO: almost all code duplicated! # TODO: almost all code duplicated!
#but parameter list is different so a simple function pointer wont work # but parameter list is different so a simple function pointer wont work
def hole(self, diameter, depth=None, clean=True): def hole(self, diameter, depth=None, clean=True):
""" """
Makes a hole for each item on the stack. Makes a hole for each item on the stack.
@ -1933,13 +1941,14 @@ class Workplane(CQ):
pnt is in local coordinates pnt is in local coordinates
""" """
boreDir = Vector(0, 0, -1) boreDir = Vector(0, 0, -1)
#first make the hole # first make the hole
hole = Solid.makeCylinder(diameter / 2.0, depth, center, boreDir) # local coordinates! hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coordinates!
return hole return hole
return self.cutEach(_makeHole, True, clean) return self.cutEach(_makeHole, True, clean)
#TODO: duplicated code with _extrude and extrude # TODO: duplicated code with _extrude and extrude
def twistExtrude(self, distance, angleDegrees, combine=True, clean=True): def twistExtrude(self, distance, angleDegrees, combine=True, clean=True):
""" """
Extrudes a wire in the direction normal to the plane, but also twists by the specified Extrudes a wire in the direction normal to the plane, but also twists by the specified
@ -1959,21 +1968,23 @@ class Workplane(CQ):
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
:return: a CQ object with the resulting solid selected. :return: a CQ object with the resulting solid selected.
""" """
#group wires together into faces based on which ones are inside the others # group wires together into faces based on which ones are inside the others
#result is a list of lists # result is a list of lists
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires), self.plane, [])
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion # now all of the wires have been used to create an extrusion
self.ctx.pendingWires = []
#compute extrusion vector and extrude # compute extrusion vector and extrude
eDir = self.plane.zDir.multiply(distance) eDir = self.plane.zDir.multiply(distance)
#one would think that fusing faces into a compound and then extruding would work, # one would think that fusing faces into a compound and then extruding would work,
#but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
#but then cutting it from the main solid fails with BRep_NotDone. # but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work # the work around is to extrude each and then join the resulting solids, which seems to work
#underlying cad kernel can only handle simple bosses-- we'll aggregate them if there # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there
# are multiple sets # are multiple sets
r = None r = None
for ws in wireSets: for ws in wireSets:
@ -1988,7 +1999,8 @@ class Workplane(CQ):
newS = self._combineWithBase(r) newS = self._combineWithBase(r)
else: else:
newS = self.newObject([r]) newS = self.newObject([r])
if clean: newS = newS.clean() if clean:
newS = newS.clean()
return newS return newS
def extrude(self, distance, combine=True, clean=True, both=False): def extrude(self, distance, combine=True, clean=True, both=False):
@ -2015,14 +2027,16 @@ class Workplane(CQ):
Support for non-prismatic extrusion ( IE, sweeping along a profile, not just 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 perpendicular to the plane extrude to surface. this is quite tricky since the surface
selected may not be planar selected may not be planar
""" """
r = self._extrude(distance,both=both) # returns a Solid (or a compound if there were multiple) r = self._extrude(
distance, both=both) # returns a Solid (or a compound if there were multiple)
if combine: if combine:
newS = self._combineWithBase(r) newS = self._combineWithBase(r)
else: else:
newS = self.newObject([r]) newS = self.newObject([r])
if clean: newS = newS.clean() if clean:
newS = newS.clean()
return newS return newS
def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True): def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True):
@ -2047,10 +2061,10 @@ class Workplane(CQ):
* if combine is true, the value is combined with the context solid if it exists, * if combine is true, the value is combined with the context solid if it exists,
and the resulting solid becomes the new context solid. and the resulting solid becomes the new context solid.
""" """
#Make sure we account for users specifying angles larger than 360 degrees # Make sure we account for users specifying angles larger than 360 degrees
angleDegrees %= 360.0 angleDegrees %= 360.0
#Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve # Compensate for FreeCAD not assuming that a 0 degree revolve means a 360 degree revolve
angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees angleDegrees = 360.0 if angleDegrees == 0 else angleDegrees
# The default start point of the vector defining the axis of rotation will be the origin # The default start point of the vector defining the axis of rotation will be the origin
@ -2078,7 +2092,8 @@ class Workplane(CQ):
newS = self._combineWithBase(r) newS = self._combineWithBase(r)
else: else:
newS = self.newObject([r]) newS = self.newObject([r])
if clean: newS = newS.clean() if clean:
newS = newS.clean()
return newS return newS
def sweep(self, path, makeSolid=True, isFrenet=False, combine=True, clean=True): def sweep(self, path, makeSolid=True, isFrenet=False, combine=True, clean=True):
@ -2091,12 +2106,14 @@ class Workplane(CQ):
:return: a CQ object with the resulting solid selected. :return: a CQ object with the resulting solid selected.
""" """
r = self._sweep(path.wire(), makeSolid, isFrenet) # returns a Solid (or a compound if there were multiple) # returns a Solid (or a compound if there were multiple)
r = self._sweep(path.wire(), makeSolid, isFrenet)
if combine: if combine:
newS = self._combineWithBase(r) newS = self._combineWithBase(r)
else: else:
newS = self.newObject([r]) newS = self.newObject([r])
if clean: newS = newS.clean() if clean:
newS = newS.clean()
return newS return newS
def _combineWithBase(self, obj): def _combineWithBase(self, obj):
@ -2128,7 +2145,8 @@ class Workplane(CQ):
for ss in items: for ss in items:
s = s.fuse(ss) s = s.fuse(ss)
if clean: s = s.clean() if clean:
s = s.clean()
return self.newObject([s]) return self.newObject([s])
@ -2146,11 +2164,12 @@ class Workplane(CQ):
:return: a CQ object with the resulting object selected :return: a CQ object with the resulting object selected
""" """
#first collect all of the items together # first collect all of the items together
if type(toUnion) == CQ or type(toUnion) == Workplane: if type(toUnion) == CQ or type(toUnion) == Workplane:
solids = toUnion.solids().vals() solids = toUnion.solids().vals()
if len(solids) < 1: if len(solids) < 1:
raise ValueError("CQ object must have at least one solid on the stack to union!") raise ValueError(
"CQ object must have at least one solid on the stack to union!")
newS = solids.pop(0) newS = solids.pop(0)
for s in solids: for s in solids:
newS = newS.fuse(s) newS = newS.fuse(s)
@ -2159,7 +2178,7 @@ class Workplane(CQ):
else: else:
raise ValueError("Cannot union type '{}'".format(type(toUnion))) raise ValueError("Cannot union type '{}'".format(type(toUnion)))
#now combine with existing solid, if there is one # now combine with existing solid, if there is one
# look for parents to cut from # look for parents to cut from
solidRef = self.findSolid(searchStack=True, searchParents=True) solidRef = self.findSolid(searchStack=True, searchParents=True)
if combine and solidRef is not None: if combine and solidRef is not None:
@ -2168,7 +2187,8 @@ class Workplane(CQ):
else: else:
r = newS r = newS
if clean: r = r.clean() if clean:
r = r.clean()
return self.newObject([r]) return self.newObject([r])
@ -2194,14 +2214,15 @@ class Workplane(CQ):
solidToCut = None solidToCut = None
if type(toCut) == CQ or type(toCut) == Workplane: if type(toCut) == CQ or type(toCut) == Workplane:
solidToCut = toCut.val() solidToCut = toCut.val()
elif type(toCut) in (Solid,Compound): elif type(toCut) in (Solid, Compound):
solidToCut = toCut solidToCut = toCut
else: else:
raise ValueError("Cannot cut type '{}'".format(type(toCut))) raise ValueError("Cannot cut type '{}'".format(type(toCut)))
newS = solidRef.cut(solidToCut) newS = solidRef.cut(solidToCut)
if clean: newS = newS.clean() if clean:
newS = newS.clean()
if combine: if combine:
solidRef.wrapped = newS.wrapped solidRef.wrapped = newS.wrapped
@ -2227,16 +2248,17 @@ class Workplane(CQ):
Future Enhancements: Future Enhancements:
Cut Up to Surface Cut Up to Surface
""" """
#first, make the object # first, make the object
toCut = self._extrude(distanceToCut) toCut = self._extrude(distanceToCut)
#now find a solid in the chain # now find a solid in the chain
solidRef = self.findSolid() solidRef = self.findSolid()
s = solidRef.cut(toCut) s = solidRef.cut(toCut)
if clean: s = s.clean() if clean:
s = s.clean()
solidRef.wrapped = s.wrapped solidRef.wrapped = s.wrapped
return self.newObject([s]) return self.newObject([s])
@ -2295,21 +2317,22 @@ class Workplane(CQ):
extrude along a profile (sweep) extrude along a profile (sweep)
""" """
#group wires together into faces based on which ones are inside the others # group wires together into faces based on which ones are inside the others
#result is a list of lists # result is a list of lists
s = time.time() s = time.time()
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) wireSets = sortWiresByBuildOrder(
#print "sorted wires in %d sec" % ( time.time() - s ) list(self.ctx.pendingWires), self.plane, [])
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion # print "sorted wires in %d sec" % ( time.time() - s )
# now all of the wires have been used to create an extrusion
self.ctx.pendingWires = []
#compute extrusion vector and extrude # compute extrusion vector and extrude
eDir = self.plane.zDir.multiply(distance) eDir = self.plane.zDir.multiply(distance)
# one would think that fusing faces into a compound and then extruding would work,
#one would think that fusing faces into a compound and then extruding would work, # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
#but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc) # but then cutting it from the main solid fails with BRep_NotDone.
#but then cutting it from the main solid fails with BRep_NotDone. # the work around is to extrude each and then join the resulting solids, which seems to work
#the work around is to extrude each and then join the resulting solids, which seems to work
# underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are # underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are
# multiple sets # multiple sets
@ -2332,9 +2355,10 @@ class Workplane(CQ):
for ws in wireSets: for ws in wireSets:
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir)
toFuse.append(thisObj) toFuse.append(thisObj)
if both: if both:
thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.)) thisObj = Solid.extrudeLinear(
ws[0], ws[1:], eDir.multiply(-1.))
toFuse.append(thisObj) toFuse.append(thisObj)
return Compound.makeCompound(toFuse) return Compound.makeCompound(toFuse)
@ -2353,16 +2377,18 @@ class Workplane(CQ):
This method is a utility method, primarily for plugin and internal use. This method is a utility method, primarily for plugin and internal use.
""" """
#We have to gather the wires to be revolved # We have to gather the wires to be revolved
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires), self.plane, [])
#Mark that all of the wires have been used to create a revolution # Mark that all of the wires have been used to create a revolution
self.ctx.pendingWires = [] self.ctx.pendingWires = []
#Revolve the wires, make a compound out of them and then fuse them # Revolve the wires, make a compound out of them and then fuse them
toFuse = [] toFuse = []
for ws in wireSets: for ws in wireSets:
thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd) thisObj = Solid.revolve(
ws[0], ws[1:], angleDegrees, axisStart, axisEnd)
toFuse.append(thisObj) toFuse.append(thisObj)
return Compound.makeCompound(toFuse) return Compound.makeCompound(toFuse)
@ -2378,13 +2404,16 @@ class Workplane(CQ):
# group wires together into faces based on which ones are inside the others # group wires together into faces based on which ones are inside the others
# result is a list of lists # result is a list of lists
s = time.time() s = time.time()
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, []) wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires), self.plane, [])
# print "sorted wires in %d sec" % ( time.time() - s ) # print "sorted wires in %d sec" % ( time.time() - s )
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion # now all of the wires have been used to create an extrusion
self.ctx.pendingWires = []
toFuse = [] toFuse = []
for ws in wireSets: for ws in wireSets:
thisObj = Solid.sweep(ws[0], ws[1:], path.val(), makeSolid, isFrenet) thisObj = Solid.sweep(
ws[0], ws[1:], path.val(), makeSolid, isFrenet)
toFuse.append(thisObj) toFuse.append(thisObj)
return Compound.makeCompound(toFuse) return Compound.makeCompound(toFuse)
@ -2447,11 +2476,11 @@ class Workplane(CQ):
boxes = self.eachpoint(_makebox, True) boxes = self.eachpoint(_makebox, True)
#if combination is not desired, just return the created boxes # if combination is not desired, just return the created boxes
if not combine: if not combine:
return boxes return boxes
else: else:
#combine everything # combine everything
return self.union(boxes, clean=clean) return self.union(boxes, clean=clean)
def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360,
@ -2547,5 +2576,6 @@ class Workplane(CQ):
try: try:
cleanObjects = [obj.clean() for obj in self.objects] cleanObjects = [obj.clean() for obj in self.objects]
except AttributeError: except AttributeError:
raise AttributeError("%s object doesn't support `clean()` method!" % obj.ShapeType()) raise AttributeError(
"%s object doesn't support `clean()` method!" % obj.ShapeType())
return self.newObject(cleanObjects) return self.newObject(cleanObjects)

View File

@ -6,7 +6,7 @@ A special directive for including a cq object.
import traceback import traceback
from cadquery import * from cadquery import *
from cadquery import cqgi from cadquery import cqgi
import StringIO import io
from docutils.parsers.rst import directives from docutils.parsers.rst import directives
template = """ template = """
@ -34,7 +34,7 @@ def cq_directive(name, arguments, options, content, lineno,
out_svg = "Your Script Did not assign call build_output() function!" out_svg = "Your Script Did not assign call build_output() function!"
try: try:
_s = StringIO.StringIO() _s = io.StringIO()
result = cqgi.parse(plot_code).build() result = cqgi.parse(plot_code).build()
if result.success: if result.success:

View File

@ -9,6 +9,7 @@ import cadquery
CQSCRIPT = "<cqscript>" CQSCRIPT = "<cqscript>"
def parse(script_source): def parse(script_source):
""" """
Parses the script as a model, and returns a model. Parses the script as a model, and returns a model.
@ -48,19 +49,19 @@ class CQModel(object):
# TODO: pick up other scirpt metadata: # TODO: pick up other scirpt metadata:
# describe # describe
# pick up validation methods # pick up validation methods
self._find_descriptions() self._find_descriptions()
def _find_vars(self): def _find_vars(self):
""" """
Parse the script, and populate variables that appear to be Parse the script, and populate variables that appear to be
overridable. overridable.
""" """
#assumption here: we assume that variable declarations # assumption here: we assume that variable declarations
#are only at the top level of the script. IE, we'll ignore any # are only at the top level of the script. IE, we'll ignore any
#variable definitions at lower levels of the script # variable definitions at lower levels of the script
#we dont want to use the visit interface because here we excplicitly # we dont want to use the visit interface because here we excplicitly
#want to walk only the top level of the tree. # want to walk only the top level of the tree.
assignment_finder = ConstantAssignmentFinder(self.metadata) assignment_finder = ConstantAssignmentFinder(self.metadata)
for node in self.ast_tree.body: for node in self.ast_tree.body:
@ -104,22 +105,23 @@ class CQModel(object):
env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \ env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
.add_entry("build_object", collector.build_object) \ .add_entry("build_object", collector.build_object) \
.add_entry("debug", collector.debug) \ .add_entry("debug", collector.debug) \
.add_entry("describe_parameter",collector.describe_parameter) \ .add_entry("describe_parameter", collector.describe_parameter) \
.build() .build()
c = compile(self.ast_tree, CQSCRIPT, 'exec') c = compile(self.ast_tree, CQSCRIPT, 'exec')
exec (c, env) exec(c, env)
result.set_debug(collector.debugObjects ) result.set_debug(collector.debugObjects)
if collector.has_results(): if collector.has_results():
result.set_success_result(collector.outputObjects) result.set_success_result(collector.outputObjects)
else: else:
raise NoOutputError("Script did not call build_object-- no output available.") raise NoOutputError(
except Exception, ex: "Script did not call build_object-- no output available.")
print "Error Executing Script:" except Exception as ex:
print("Error Executing Script:")
result.set_failure_result(ex) result.set_failure_result(ex)
traceback.print_exc() traceback.print_exc()
print "Full Text of Script:" print("Full Text of Script:")
print self.script_source print(self.script_source)
end = time.clock() end = time.clock()
result.buildTime = end - start result.buildTime = end - start
@ -128,9 +130,10 @@ class CQModel(object):
def set_param_values(self, params): def set_param_values(self, params):
model_parameters = self.metadata.parameters model_parameters = self.metadata.parameters
for k, v in params.iteritems(): for k, v in params.items():
if k not in model_parameters: if k not in model_parameters:
raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k) raise InvalidParameterError(
"Cannot set value '%s': not a parameter of the model." % k)
p = model_parameters[k] p = model_parameters[k]
p.set_value(v) p.set_value(v)
@ -147,6 +150,7 @@ class BuildResult(object):
If unsuccessful, the exception property contains a reference to If unsuccessful, the exception property contains a reference to
the stack trace that occurred. the stack trace that occurred.
""" """
def __init__(self): def __init__(self):
self.buildTime = None self.buildTime = None
self.results = [] self.results = []
@ -173,14 +177,15 @@ class ScriptMetadata(object):
Defines the metadata for a parsed CQ Script. Defines the metadata for a parsed CQ Script.
the parameters property is a dict of InputParameter objects. the parameters property is a dict of InputParameter objects.
""" """
def __init__(self): def __init__(self):
self.parameters = {} self.parameters = {}
def add_script_parameter(self, p): def add_script_parameter(self, p):
self.parameters[p.name] = p self.parameters[p.name] = p
def add_parameter_description(self,name,description): def add_parameter_description(self, name, description):
print 'Adding Parameter name=%s, desc=%s' % ( name, description ) print('Adding Parameter name=%s, desc=%s' % (name, description))
p = self.parameters[name] p = self.parameters[name]
p.desc = description p.desc = description
@ -212,6 +217,7 @@ class InputParameter:
provide additional metadata provide additional metadata
""" """
def __init__(self): def __init__(self):
#: the default value for the variable. #: the default value for the variable.
@ -251,7 +257,7 @@ class InputParameter:
if len(self.valid_values) > 0 and new_value not in self.valid_values: if len(self.valid_values) > 0 and new_value not in self.valid_values:
raise InvalidParameterError( raise InvalidParameterError(
"Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} " "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} "
.format(str(new_value), self.name, str(self.valid_values))) .format(str(new_value), self.name, str(self.valid_values)))
if self.varType == NumberParameterType: if self.varType == NumberParameterType:
try: try:
@ -260,7 +266,7 @@ class InputParameter:
except ValueError: except ValueError:
raise InvalidParameterError( raise InvalidParameterError(
"Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric." "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric."
.format(str(new_value), self.name)) .format(str(new_value), self.name))
elif self.varType == StringParameterType: elif self.varType == StringParameterType:
self.ast_node.s = str(new_value) self.ast_node.s = str(new_value)
@ -283,6 +289,7 @@ class ScriptCallback(object):
the build_object() method is exposed to CQ scripts, to allow them the build_object() method is exposed to CQ scripts, to allow them
to return objects to the execution environment to return objects to the execution environment
""" """
def __init__(self): def __init__(self):
self.outputObjects = [] self.outputObjects = []
self.debugObjects = [] self.debugObjects = []
@ -294,13 +301,13 @@ class ScriptCallback(object):
""" """
self.outputObjects.append(shape) self.outputObjects.append(shape)
def debug(self,obj,args={}): def debug(self, obj, args={}):
""" """
Debug print/output an object, with optional arguments. Debug print/output an object, with optional arguments.
""" """
self.debugObjects.append(DebugObject(obj,args)) self.debugObjects.append(DebugObject(obj, args))
def describe_parameter(self,var_data ): def describe_parameter(self, var_data):
""" """
Do Nothing-- we parsed the ast ahead of exection to get what we need. Do Nothing-- we parsed the ast ahead of exection to get what we need.
""" """
@ -315,16 +322,19 @@ class ScriptCallback(object):
def has_results(self): def has_results(self):
return len(self.outputObjects) > 0 return len(self.outputObjects) > 0
class DebugObject(object): class DebugObject(object):
""" """
Represents a request to debug an object Represents a request to debug an object
Object is the type of object we want to debug Object is the type of object we want to debug
args are parameters for use during debuging ( for example, color, tranparency ) args are parameters for use during debuging ( for example, color, tranparency )
""" """
def __init__(self,object,args):
def __init__(self, object, args):
self.args = args self.args = args
self.object = object self.object = object
class InvalidParameterError(Exception): class InvalidParameterError(Exception):
""" """
Raised when an attempt is made to provide a new parameter value Raised when an attempt is made to provide a new parameter value
@ -375,6 +385,7 @@ class EnvironmentBuilder(object):
The environment includes the builtins, as well as The environment includes the builtins, as well as
the other methods the script will need. the other methods the script will need.
""" """
def __init__(self): def __init__(self):
self.env = {} self.env = {}
@ -397,30 +408,33 @@ class EnvironmentBuilder(object):
def build(self): def build(self):
return self.env return self.env
class ParameterDescriptionFinder(ast.NodeTransformer): class ParameterDescriptionFinder(ast.NodeTransformer):
""" """
Visits a parse tree, looking for function calls to describe_parameter(var, description ) Visits a parse tree, looking for function calls to describe_parameter(var, description )
""" """
def __init__(self, cq_model): def __init__(self, cq_model):
self.cqModel = cq_model self.cqModel = cq_model
def visit_Call(self,node): def visit_Call(self, node):
""" """
Called when we see a function call. Is it describe_parameter? Called when we see a function call. Is it describe_parameter?
""" """
try: try:
if node.func.id == 'describe_parameter': if node.func.id == 'describe_parameter':
#looks like we have a call to our function. # looks like we have a call to our function.
#first parameter is the variable, # first parameter is the variable,
#second is the description # second is the description
varname = node.args[0].id varname = node.args[0].id
desc = node.args[1].s desc = node.args[1].s
self.cqModel.add_parameter_description(varname,desc) self.cqModel.add_parameter_description(varname, desc)
except: except:
print "Unable to handle function call" print("Unable to handle function call")
pass pass
return node return node
class ConstantAssignmentFinder(ast.NodeTransformer): class ConstantAssignmentFinder(ast.NodeTransformer):
""" """
@ -447,7 +461,7 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
self.cqModel.add_script_parameter( self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True)) InputParameter.create(value_node, var_name, BooleanParameterType, True))
except: except:
print "Unable to handle assignment for variable '%s'" % var_name print("Unable to handle assignment for variable '%s'" % var_name)
pass pass
def visit_Assign(self, node): def visit_Assign(self, node):
@ -455,8 +469,8 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
try: try:
left_side = node.targets[0] left_side = node.targets[0]
#do not handle attribute assignments # do not handle attribute assignments
if isinstance(left_side,ast.Attribute): if isinstance(left_side, ast.Attribute):
return return
if type(node.value) in [ast.Num, ast.Str, ast.Name]: if type(node.value) in [ast.Num, ast.Str, ast.Name]:
@ -467,6 +481,7 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
self.handle_assignment(n.id, v) self.handle_assignment(n.id, v)
except: except:
traceback.print_exc() traceback.print_exc()
print "Unable to handle assignment for node '%s'" % ast.dump(left_side) print("Unable to handle assignment for node '%s'" %
ast.dump(left_side))
return node return node

View File

@ -37,7 +37,7 @@ def _fc_path():
"/usr/bin/freecad/lib", "/usr/bin/freecad/lib",
"/usr/lib/freecad", "/usr/lib/freecad",
"/usr/lib64/freecad/lib", "/usr/lib64/freecad/lib",
]: ]:
if os.path.exists(_PATH): if os.path.exists(_PATH):
return _PATH return _PATH
@ -80,7 +80,7 @@ def _fc_path():
"c:/apps/FreeCAD 0.15/bin", "c:/apps/FreeCAD 0.15/bin",
"c:/apps/FreeCAD 0.16/bin", "c:/apps/FreeCAD 0.16/bin",
"c:/apps/FreeCAD 0.17/bin", "c:/apps/FreeCAD 0.17/bin",
]: ]:
if os.path.exists(_PATH): if os.path.exists(_PATH):
return _PATH return _PATH
@ -90,7 +90,7 @@ def _fc_path():
"/Applications/FreeCAD.app/Contents/lib", "/Applications/FreeCAD.app/Contents/lib",
os.path.join(os.path.expanduser("~"), os.path.join(os.path.expanduser("~"),
"Library/Application Support/FreeCAD/lib"), "Library/Application Support/FreeCAD/lib"),
]: ]:
if os.path.exists(_PATH): if os.path.exists(_PATH):
return _PATH return _PATH

View File

@ -3,7 +3,9 @@ import cadquery
import FreeCAD import FreeCAD
import Drawing import Drawing
import tempfile, os, StringIO import tempfile
import os
import io
try: try:
@ -26,12 +28,12 @@ class UNITS:
def toString(shape, exportType, tolerance=0.1): def toString(shape, exportType, tolerance=0.1):
s = StringIO.StringIO() s = io.StringIO()
exportShape(shape, exportType, s, tolerance) exportShape(shape, exportType, s, tolerance)
return s.getvalue() return s.getvalue()
def exportShape(shape,exportType,fileLike,tolerance=0.1): def exportShape(shape, exportType, fileLike, tolerance=0.1):
""" """
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery :param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
object, the first value is exported object, the first value is exported
@ -42,23 +44,22 @@ def exportShape(shape,exportType,fileLike,tolerance=0.1):
for closing the object for closing the object
""" """
if isinstance(shape, cadquery.CQ):
if isinstance(shape,cadquery.CQ):
shape = shape.val() shape = shape.val()
if exportType == ExportTypes.TJS: if exportType == ExportTypes.TJS:
#tessellate the model # tessellate the model
tess = shape.tessellate(tolerance) tess = shape.tessellate(tolerance)
mesher = JsonMesh() #warning: needs to be changed to remove buildTime and exportTime!!! mesher = JsonMesh() # warning: needs to be changed to remove buildTime and exportTime!!!
#add vertices # add vertices
for vec in tess[0]: for vec in tess[0]:
mesher.addVertex(vec.x, vec.y, vec.z) mesher.addVertex(vec.x, vec.y, vec.z)
#add faces # add faces
for f in tess[1]: for f in tess[1]:
mesher.addTriangleFace(f[0],f[1], f[2]) mesher.addTriangleFace(f[0], f[1], f[2])
fileLike.write( mesher.toJson()) fileLike.write(mesher.toJson())
elif exportType == ExportTypes.SVG: elif exportType == ExportTypes.SVG:
fileLike.write(getSVG(shape.wrapped)) fileLike.write(getSVG(shape.wrapped))
elif exportType == ExportTypes.AMF: elif exportType == ExportTypes.AMF:
@ -66,11 +67,11 @@ def exportShape(shape,exportType,fileLike,tolerance=0.1):
aw = AmfWriter(tess).writeAmf(fileLike) aw = AmfWriter(tess).writeAmf(fileLike)
else: else:
#all these types required writing to a file and then # all these types required writing to a file and then
#re-reading. this is due to the fact that FreeCAD writes these # re-reading. this is due to the fact that FreeCAD writes these
(h, outFileName) = tempfile.mkstemp() (h, outFileName) = tempfile.mkstemp()
#weird, but we need to close this file. the next step is going to write to # weird, but we need to close this file. the next step is going to write to
#it from c code, so it needs to be closed. # it from c code, so it needs to be closed.
os.close(h) os.close(h)
if exportType == ExportTypes.STEP: if exportType == ExportTypes.STEP:
@ -83,13 +84,14 @@ def exportShape(shape,exportType,fileLike,tolerance=0.1):
res = readAndDeleteFile(outFileName) res = readAndDeleteFile(outFileName)
fileLike.write(res) fileLike.write(res)
def readAndDeleteFile(fileName): def readAndDeleteFile(fileName):
""" """
read data from file provided, and delete it when done read data from file provided, and delete it when done
return the contents as a string return the contents as a string
""" """
res = "" res = ""
with open(fileName,'r') as f: with open(fileName, 'r') as f:
res = f.read() res = f.read()
os.remove(fileName) os.remove(fileName)
@ -102,16 +104,16 @@ def guessUnitOfMeasure(shape):
""" """
bb = shape.BoundBox bb = shape.BoundBox
dimList = [ bb.XLength, bb.YLength,bb.ZLength ] dimList = [bb.XLength, bb.YLength, bb.ZLength]
#no real part would likely be bigger than 10 inches on any side # no real part would likely be bigger than 10 inches on any side
if max(dimList) > 10: if max(dimList) > 10:
return UNITS.MM return UNITS.MM
#no real part would likely be smaller than 0.1 mm on all dimensions # no real part would likely be smaller than 0.1 mm on all dimensions
if min(dimList) < 0.1: if min(dimList) < 0.1:
return UNITS.IN return UNITS.IN
#no real part would have the sum of its dimensions less than about 5mm # no real part would have the sum of its dimensions less than about 5mm
if sum(dimList) < 10: if sum(dimList) < 10:
return UNITS.IN return UNITS.IN
@ -119,76 +121,79 @@ def guessUnitOfMeasure(shape):
class AmfWriter(object): class AmfWriter(object):
def __init__(self,tessellation): def __init__(self, tessellation):
self.units = "mm" self.units = "mm"
self.tessellation = tessellation self.tessellation = tessellation
def writeAmf(self,outFile): def writeAmf(self, outFile):
amf = ET.Element('amf',units=self.units) amf = ET.Element('amf', units=self.units)
#TODO: if result is a compound, we need to loop through them # TODO: if result is a compound, we need to loop through them
object = ET.SubElement(amf,'object',id="0") object = ET.SubElement(amf, 'object', id="0")
mesh = ET.SubElement(object,'mesh') mesh = ET.SubElement(object, 'mesh')
vertices = ET.SubElement(mesh,'vertices') vertices = ET.SubElement(mesh, 'vertices')
volume = ET.SubElement(mesh,'volume') volume = ET.SubElement(mesh, 'volume')
#add vertices # add vertices
for v in self.tessellation[0]: for v in self.tessellation[0]:
vtx = ET.SubElement(vertices,'vertex') vtx = ET.SubElement(vertices, 'vertex')
coord = ET.SubElement(vtx,'coordinates') coord = ET.SubElement(vtx, 'coordinates')
x = ET.SubElement(coord,'x') x = ET.SubElement(coord, 'x')
x.text = str(v.x) x.text = str(v.x)
y = ET.SubElement(coord,'y') y = ET.SubElement(coord, 'y')
y.text = str(v.y) y.text = str(v.y)
z = ET.SubElement(coord,'z') z = ET.SubElement(coord, 'z')
z.text = str(v.z) z.text = str(v.z)
#add triangles # add triangles
for t in self.tessellation[1]: for t in self.tessellation[1]:
triangle = ET.SubElement(volume,'triangle') triangle = ET.SubElement(volume, 'triangle')
v1 = ET.SubElement(triangle,'v1') v1 = ET.SubElement(triangle, 'v1')
v1.text = str(t[0]) v1.text = str(t[0])
v2 = ET.SubElement(triangle,'v2') v2 = ET.SubElement(triangle, 'v2')
v2.text = str(t[1]) v2.text = str(t[1])
v3 = ET.SubElement(triangle,'v3') v3 = ET.SubElement(triangle, 'v3')
v3.text = str(t[2]) v3.text = str(t[2])
ET.ElementTree(amf).write(outFile, encoding='ISO-8859-1')
ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1')
""" """
Objects that represent Objects that represent
three.js JSON object notation three.js JSON object notation
https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0 https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
""" """
class JsonMesh(object): class JsonMesh(object):
def __init__(self): def __init__(self):
self.vertices = []; self.vertices = []
self.faces = []; self.faces = []
self.nVertices = 0; self.nVertices = 0
self.nFaces = 0; self.nFaces = 0
def addVertex(self,x,y,z): def addVertex(self, x, y, z):
self.nVertices += 1; self.nVertices += 1
self.vertices.extend([x,y,z]); self.vertices.extend([x, y, z])
#add triangle composed of the three provided vertex indices # add triangle composed of the three provided vertex indices
def addTriangleFace(self, i,j,k): def addTriangleFace(self, i, j, k):
#first position means justa simple triangle # first position means justa simple triangle
self.nFaces += 1; self.nFaces += 1
self.faces.extend([0,int(i),int(j),int(k)]); self.faces.extend([0, int(i), int(j), int(k)])
""" """
Get a json model from this model. Get a json model from this model.
For now we'll forget about colors, vertex normals, and all that stuff For now we'll forget about colors, vertex normals, and all that stuff
""" """
def toJson(self): def toJson(self):
return JSON_TEMPLATE % { return JSON_TEMPLATE % {
'vertices' : str(self.vertices), 'vertices': str(self.vertices),
'faces' : str(self.faces), 'faces': str(self.faces),
'nVertices': self.nVertices, 'nVertices': self.nVertices,
'nFaces' : self.nFaces 'nFaces': self.nFaces
}; };
@ -210,62 +215,64 @@ def getPaths(freeCadSVG):
hiddenPaths = [] hiddenPaths = []
visiblePaths = [] visiblePaths = []
if len(freeCadSVG) > 0: if len(freeCadSVG) > 0:
#yuk, freecad returns svg fragments. stupid stupid # yuk, freecad returns svg fragments. stupid stupid
fullDoc = "<root>%s</root>" % freeCadSVG fullDoc = "<root>%s</root>" % freeCadSVG
e = ET.ElementTree(ET.fromstring(fullDoc)) e = ET.ElementTree(ET.fromstring(fullDoc))
segments = e.findall(".//g") segments = e.findall(".//g")
for s in segments: for s in segments:
paths = s.findall("path") paths = s.findall("path")
if s.get("stroke-width") == "0.15": #hidden line HACK HACK HACK if s.get("stroke-width") == "0.15": # hidden line HACK HACK HACK
mylist = hiddenPaths mylist = hiddenPaths
else: else:
mylist = visiblePaths mylist = visiblePaths
for p in paths: for p in paths:
mylist.append(p.get("d")) mylist.append(p.get("d"))
return (hiddenPaths,visiblePaths) return (hiddenPaths, visiblePaths)
else: else:
return ([],[]) return ([], [])
def getSVG(shape,opts=None): def getSVG(shape, opts=None):
""" """
Export a shape to SVG Export a shape to SVG
""" """
d = {'width':800,'height':240,'marginLeft':200,'marginTop':20} d = {'width': 800, 'height': 240, 'marginLeft': 200, 'marginTop': 20}
if opts: if opts:
d.update(opts) d.update(opts)
#need to guess the scale and the coordinate center # need to guess the scale and the coordinate center
uom = guessUnitOfMeasure(shape) uom = guessUnitOfMeasure(shape)
width=float(d['width']) width = float(d['width'])
height=float(d['height']) height = float(d['height'])
marginLeft=float(d['marginLeft']) marginLeft = float(d['marginLeft'])
marginTop=float(d['marginTop']) marginTop = float(d['marginTop'])
#TODO: provide option to give 3 views # TODO: provide option to give 3 views
viewVector = FreeCAD.Base.Vector(-1.75,1.1,5) viewVector = FreeCAD.Base.Vector(-1.75, 1.1, 5)
(visibleG0,visibleG1,hiddenG0,hiddenG1) = Drawing.project(shape,viewVector) (visibleG0, visibleG1, hiddenG0, hiddenG1) = Drawing.project(shape, viewVector)
(hiddenPaths,visiblePaths) = getPaths(Drawing.projectToSVG(shape,viewVector,"ShowHiddenLines")) #this param is totally undocumented! (hiddenPaths, visiblePaths) = getPaths(Drawing.projectToSVG(
shape, viewVector, "ShowHiddenLines")) # this param is totally undocumented!
#get bounding box -- these are all in 2-d space # get bounding box -- these are all in 2-d space
bb = visibleG0.BoundBox bb = visibleG0.BoundBox
bb.add(visibleG1.BoundBox) bb.add(visibleG1.BoundBox)
bb.add(hiddenG0.BoundBox) bb.add(hiddenG0.BoundBox)
bb.add(hiddenG1.BoundBox) bb.add(hiddenG1.BoundBox)
#width pixels for x, height pixesl for y # width pixels for x, height pixesl for y
unitScale = min( width / bb.XLength * 0.75 , height / bb.YLength * 0.75 ) unitScale = min(width / bb.XLength * 0.75, height / bb.YLength * 0.75)
#compute amount to translate-- move the top left into view # compute amount to translate-- move the top left into view
(xTranslate,yTranslate) = ( (0 - bb.XMin) + marginLeft/unitScale ,(0- bb.YMax) - marginTop/unitScale) (xTranslate, yTranslate) = ((0 - bb.XMin) + marginLeft /
unitScale, (0 - bb.YMax) - marginTop / unitScale)
#compute paths ( again -- had to strip out freecad crap ) # compute paths ( again -- had to strip out freecad crap )
hiddenContent = "" hiddenContent = ""
for p in hiddenPaths: for p in hiddenPaths:
hiddenContent += PATHTEMPLATE % p hiddenContent += PATHTEMPLATE % p
@ -274,21 +281,21 @@ def getSVG(shape,opts=None):
for p in visiblePaths: for p in visiblePaths:
visibleContent += PATHTEMPLATE % p visibleContent += PATHTEMPLATE % p
svg = SVG_TEMPLATE % ( svg = SVG_TEMPLATE % (
{ {
"unitScale" : str(unitScale), "unitScale": str(unitScale),
"strokeWidth" : str(1.0/unitScale), "strokeWidth": str(1.0 / unitScale),
"hiddenContent" : hiddenContent , "hiddenContent": hiddenContent,
"visibleContent" :visibleContent, "visibleContent": visibleContent,
"xTranslate" : str(xTranslate), "xTranslate": str(xTranslate),
"yTranslate" : str(yTranslate), "yTranslate": str(yTranslate),
"width" : str(width), "width": str(width),
"height" : str(height), "height": str(height),
"textboxY" :str(height - 30), "textboxY": str(height - 30),
"uom" : str(uom) "uom": str(uom)
} }
) )
#svg = SVG_TEMPLATE % ( # svg = SVG_TEMPLATE % (
# {"content": projectedContent} # {"content": projectedContent}
#) #)
return svg return svg
@ -302,13 +309,12 @@ def exportSVG(shape, fileName):
""" """
svg = getSVG(shape.val().wrapped) svg = getSVG(shape.val().wrapped)
f = open(fileName,'w') f = open(fileName, 'w')
f.write(svg) f.write(svg)
f.close() f.close()
JSON_TEMPLATE = """\
JSON_TEMPLATE= """\
{ {
"metadata" : "metadata" :
{ {
@ -388,5 +394,4 @@ SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
</svg> </svg>
""" """
PATHTEMPLATE="\t\t\t<path d=\"%s\" />\n" PATHTEMPLATE = "\t\t\t<path d=\"%s\" />\n"

View File

@ -67,6 +67,7 @@ class Vector(object):
* a 3-tuple * a 3-tuple
* three float values, x, y, and z * three float values, x, y, and z
""" """
def __init__(self, *args): def __init__(self, *args):
if len(args) == 3: if len(args) == 3:
fV = FreeCAD.Base.Vector(args[0], args[1], args[2]) fV = FreeCAD.Base.Vector(args[0], args[1], args[2])
@ -82,7 +83,8 @@ class Vector(object):
elif len(args) == 0: elif len(args) == 0:
fV = FreeCAD.Base.Vector(0, 0, 0) fV = FreeCAD.Base.Vector(0, 0, 0)
else: else:
raise ValueError("Expected three floats, FreeCAD Vector, or 3-tuple") raise ValueError(
"Expected three floats, FreeCAD Vector, or 3-tuple")
self._wrapped = fV self._wrapped = fV
@ -147,16 +149,20 @@ class Vector(object):
return self.wrapped.getAngle(v.wrapped) return self.wrapped.getAngle(v.wrapped)
def distanceToLine(self): def distanceToLine(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def projectToLine(self): def projectToLine(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def distanceToPlane(self): def distanceToPlane(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def projectToPlane(self): def projectToPlane(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def __add__(self, v): def __add__(self, v):
return self.add(v) return self.add(v)
@ -179,6 +185,7 @@ class Matrix:
Used to move geometry in space. Used to move geometry in space.
""" """
def __init__(self, matrix=None): def __init__(self, matrix=None):
if matrix is None: if matrix is None:
self.wrapped = FreeCAD.Base.Matrix() self.wrapped = FreeCAD.Base.Matrix()
@ -255,7 +262,7 @@ class Plane(object):
return namedPlanes[stdName] return namedPlanes[stdName]
except KeyError: except KeyError:
raise ValueError('Supported names are {}'.format( raise ValueError('Supported names are {}'.format(
namedPlanes.keys())) list(namedPlanes.keys())))
@classmethod @classmethod
def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
@ -580,6 +587,7 @@ class Plane(object):
class BoundBox(object): class BoundBox(object):
"""A BoundingBox for an object or set of objects. Wraps the FreeCAD one""" """A BoundingBox for an object or set of objects. Wraps the FreeCAD one"""
def __init__(self, bb): def __init__(self, bb):
self.wrapped = bb self.wrapped = bb
self.xmin = bb.XMin self.xmin = bb.XMin
@ -631,13 +639,13 @@ class BoundBox(object):
if (fc_bb1.XMin < fc_bb2.XMin and if (fc_bb1.XMin < fc_bb2.XMin and
fc_bb1.XMax > fc_bb2.XMax and fc_bb1.XMax > fc_bb2.XMax and
fc_bb1.YMin < fc_bb2.YMin and fc_bb1.YMin < fc_bb2.YMin and
fc_bb1.YMax > fc_bb2.YMax): fc_bb1.YMax > fc_bb2.YMax):
return b1 return b1
if (fc_bb2.XMin < fc_bb1.XMin and if (fc_bb2.XMin < fc_bb1.XMin and
fc_bb2.XMax > fc_bb1.XMax and fc_bb2.XMax > fc_bb1.XMax and
fc_bb2.YMin < fc_bb1.YMin and fc_bb2.YMin < fc_bb1.YMin and
fc_bb2.YMax > fc_bb1.YMax): fc_bb2.YMax > fc_bb1.YMax):
return b2 return b2
return None return None

View File

@ -8,10 +8,12 @@ import sys
import os import os
import urllib as urlreader import urllib as urlreader
import tempfile import tempfile
class ImportTypes: class ImportTypes:
STEP = "STEP" STEP = "STEP"
class UNITS: class UNITS:
MM = "mm" MM = "mm"
IN = "in" IN = "in"
@ -24,23 +26,23 @@ def importShape(importType, fileName):
:param fileName: THe name of the file that we're importing :param fileName: THe name of the file that we're importing
""" """
#Check to see what type of file we're working with # Check to see what type of file we're working with
if importType == ImportTypes.STEP: if importType == ImportTypes.STEP:
return importStep(fileName) return importStep(fileName)
#Loads a STEP file into a CQ.Workplane object # Loads a STEP file into a CQ.Workplane object
def importStep(fileName): def importStep(fileName):
""" """
Accepts a file name and loads the STEP file into a cadquery shape Accepts a file name and loads the STEP file into a cadquery shape
:param fileName: The path and name of the STEP file to be imported :param fileName: The path and name of the STEP file to be imported
""" """
#Now read and return the shape # Now read and return the shape
try: try:
#print fileName # print fileName
rshape = Part.read(fileName) rshape = Part.read(fileName)
#Make sure that we extract all the solids # Make sure that we extract all the solids
solids = [] solids = []
for solid in rshape.Solids: for solid in rshape.Solids:
solids.append(Shape.cast(solid)) solids.append(Shape.cast(solid))
@ -49,23 +51,26 @@ def importStep(fileName):
except: except:
raise ValueError("STEP File Could not be loaded") raise ValueError("STEP File Could not be loaded")
#Loads a STEP file from an URL into a CQ.Workplane object # Loads a STEP file from an URL into a CQ.Workplane object
def importStepFromURL(url):
#Now read and return the shape
def importStepFromURL(url):
# Now read and return the shape
try: try:
webFile = urlreader.urlopen(url) webFile = urlreader.urlopen(url)
tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False) tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False)
tempFile.write(webFile.read()) tempFile.write(webFile.read())
webFile.close() webFile.close()
tempFile.close() tempFile.close()
rshape = Part.read(tempFile.name) rshape = Part.read(tempFile.name)
#Make sure that we extract all the solids # Make sure that we extract all the solids
solids = [] solids = []
for solid in rshape.Solids: for solid in rshape.Solids:
solids.append(Shape.cast(solid)) solids.append(Shape.cast(solid))
return cadquery.Workplane("XY").newObject(solids) return cadquery.Workplane("XY").newObject(solids)
except: except:
raise ValueError("STEP File from the URL: " + url + " Could not be loaded") raise ValueError("STEP File from the URL: " +
url + " Could not be loaded")

View File

@ -89,7 +89,7 @@ class Shape(object):
elif s == 'Solid': elif s == 'Solid':
tr = Solid(obj) tr = Solid(obj)
elif s == 'Compound': elif s == 'Compound':
#compound of solids, lets return a solid instead # compound of solids, lets return a solid instead
if len(obj.Solids) > 1: if len(obj.Solids) > 1:
tr = Solid(obj) tr = Solid(obj)
elif len(obj.Solids) == 1: elif len(obj.Solids) == 1:
@ -122,7 +122,7 @@ class Shape(object):
self.wrapped.exportStep(fileName) self.wrapped.exportStep(fileName)
elif fileFormat == ExportFormats.AMF: elif fileFormat == ExportFormats.AMF:
# not built into FreeCAD # not built into FreeCAD
#TODO: user selected tolerance # TODO: user selected tolerance
tess = self.wrapped.tessellate(0.1) tess = self.wrapped.tessellate(0.1)
aw = amfUtils.AMFWriter(tess) aw = amfUtils.AMFWriter(tess)
aw.writeAmf(fileName) aw.writeAmf(fileName)
@ -189,7 +189,7 @@ class Shape(object):
return BoundBox(self.wrapped.BoundBox) return BoundBox(self.wrapped.BoundBox)
def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)): def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)):
if mirrorPlane == "XY" or mirrorPlane== "YX": if mirrorPlane == "XY" or mirrorPlane == "YX":
mirrorPlaneNormalVector = FreeCAD.Base.Vector(0, 0, 1) mirrorPlaneNormalVector = FreeCAD.Base.Vector(0, 0, 1)
elif mirrorPlane == "XZ" or mirrorPlane == "ZX": elif mirrorPlane == "XZ" or mirrorPlane == "ZX":
mirrorPlaneNormalVector = FreeCAD.Base.Vector(0, 1, 0) mirrorPlaneNormalVector = FreeCAD.Base.Vector(0, 1, 0)
@ -214,9 +214,10 @@ class Shape(object):
elif isinstance(self.wrapped, FreeCADPart.Solid): elif isinstance(self.wrapped, FreeCADPart.Solid):
return Vector(self.wrapped.CenterOfMass) return Vector(self.wrapped.CenterOfMass)
else: else:
raise ValueError("Cannot find the center of %s object type" % str(type(self.Solids()[0].wrapped))) raise ValueError("Cannot find the center of %s object type" % str(
type(self.Solids()[0].wrapped)))
def CenterOfBoundBox(self, tolerance = 0.1): def CenterOfBoundBox(self, tolerance=0.1):
self.wrapped.tessellate(tolerance) self.wrapped.tessellate(tolerance)
if isinstance(self.wrapped, FreeCADPart.Shape): if isinstance(self.wrapped, FreeCADPart.Shape):
# If there are no Solids, we're probably dealing with a Face or something similar # If there are no Solids, we're probably dealing with a Face or something similar
@ -229,7 +230,8 @@ class Shape(object):
elif isinstance(self.wrapped, FreeCADPart.Solid): elif isinstance(self.wrapped, FreeCADPart.Solid):
return Vector(self.wrapped.BoundBox.Center) return Vector(self.wrapped.BoundBox.Center)
else: else:
raise ValueError("Cannot find the center(BoundBox's) of %s object type" % str(type(self.Solids()[0].wrapped))) raise ValueError("Cannot find the center(BoundBox's) of %s object type" % str(
type(self.Solids()[0].wrapped)))
@staticmethod @staticmethod
def CombinedCenter(objects): def CombinedCenter(objects):
@ -239,13 +241,14 @@ class Shape(object):
:param objects: a list of objects with mass :param objects: a list of objects with mass
""" """
total_mass = sum(Shape.computeMass(o) for o in objects) total_mass = sum(Shape.computeMass(o) for o in objects)
weighted_centers = [o.wrapped.CenterOfMass.multiply(Shape.computeMass(o)) for o in objects] weighted_centers = [o.wrapped.CenterOfMass.multiply(
Shape.computeMass(o)) for o in objects]
sum_wc = weighted_centers[0] sum_wc = weighted_centers[0]
for wc in weighted_centers[1:] : for wc in weighted_centers[1:]:
sum_wc = sum_wc.add(wc) sum_wc = sum_wc.add(wc)
return Vector(sum_wc.multiply(1./total_mass)) return Vector(sum_wc.multiply(1. / total_mass))
@staticmethod @staticmethod
def computeMass(object): def computeMass(object):
@ -254,12 +257,12 @@ class Shape(object):
in FreeCAD >=15, faces no longer have mass, but instead have area. in FreeCAD >=15, faces no longer have mass, but instead have area.
""" """
if object.wrapped.ShapeType == 'Face': if object.wrapped.ShapeType == 'Face':
return object.wrapped.Area return object.wrapped.Area
else: else:
return object.wrapped.Mass return object.wrapped.Mass
@staticmethod @staticmethod
def CombinedCenterOfBoundBox(objects, tolerance = 0.1): def CombinedCenterOfBoundBox(objects, tolerance=0.1):
""" """
Calculates the center of BoundBox of multiple objects. Calculates the center of BoundBox of multiple objects.
@ -273,10 +276,10 @@ class Shape(object):
weighted_centers.append(o.wrapped.BoundBox.Center.multiply(1.0)) weighted_centers.append(o.wrapped.BoundBox.Center.multiply(1.0))
sum_wc = weighted_centers[0] sum_wc = weighted_centers[0]
for wc in weighted_centers[1:] : for wc in weighted_centers[1:]:
sum_wc = sum_wc.add(wc) sum_wc = sum_wc.add(wc)
return Vector(sum_wc.multiply(1./total_mass)) return Vector(sum_wc.multiply(1. / total_mass))
def Closed(self): def Closed(self):
return self.wrapped.Closed return self.wrapped.Closed
@ -393,7 +396,7 @@ class Vertex(Shape):
self.Y = obj.Y self.Y = obj.Y
self.Z = obj.Z self.Z = obj.Z
# Helps identify this solid through the use of an ID # Helps identify this solid through the use of an ID
self.label = "" self.label = ""
def toTuple(self): def toTuple(self):
@ -425,12 +428,12 @@ class Edge(Shape):
FreeCADPart.Circle: 'CIRCLE' FreeCADPart.Circle: 'CIRCLE'
} }
# Helps identify this solid through the use of an ID # Helps identify this solid through the use of an ID
self.label = "" self.label = ""
def geomType(self): def geomType(self):
t = type(self.wrapped.Curve) t = type(self.wrapped.Curve)
if self.edgetypes.has_key(t): if t in self.edgetypes:
return self.edgetypes[t] return self.edgetypes[t]
else: else:
return "Unknown Edge Curve Type: %s" % str(t) return "Unknown Edge Curve Type: %s" % str(t)
@ -476,7 +479,7 @@ class Edge(Shape):
@classmethod @classmethod
def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360): def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360):
center = Vector(pnt) center = Vector(pnt)
normal = Vector(dir) normal = Vector(dir)
return Edge(FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped, angle1, angle2)) return Edge(FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped, angle1, angle2))
@classmethod @classmethod
@ -529,7 +532,7 @@ class Wire(Shape):
""" """
self.wrapped = obj self.wrapped = obj
# Helps identify this solid through the use of an ID # Helps identify this solid through the use of an ID
self.label = "" self.label = ""
@classmethod @classmethod
@ -565,7 +568,8 @@ class Wire(Shape):
:param normal: vector representing the direction of the plane the circle should lie in :param normal: vector representing the direction of the plane the circle should lie in
:return: :return:
""" """
w = Wire(FreeCADPart.Wire([FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped)])) w = Wire(FreeCADPart.Wire(
[FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped)]))
return w return w
@classmethod @classmethod
@ -588,10 +592,12 @@ class Wire(Shape):
"""This method is not implemented yet.""" """This method is not implemented yet."""
return self return self
class Face(Shape): class Face(Shape):
""" """
a bounded surface that represents part of the boundary of a solid a bounded surface that represents part of the boundary of a solid
""" """
def __init__(self, obj): def __init__(self, obj):
self.wrapped = obj self.wrapped = obj
@ -603,12 +609,12 @@ class Face(Shape):
FreeCADPart.Cone: 'CONE' FreeCADPart.Cone: 'CONE'
} }
# Helps identify this solid through the use of an ID # Helps identify this solid through the use of an ID
self.label = "" self.label = ""
def geomType(self): def geomType(self):
t = type(self.wrapped.Surface) t = type(self.wrapped.Surface)
if self.facetypes.has_key(t): if t in self.facetypes:
return self.facetypes[t] return self.facetypes[t]
else: else:
return "Unknown Face Surface Type: %s" % str(t) return "Unknown Face Surface Type: %s" % str(t)
@ -661,13 +667,14 @@ class Shell(Shape):
""" """
the outer boundary of a surface the outer boundary of a surface
""" """
def __init__(self, wrapped): def __init__(self, wrapped):
""" """
A Shell A Shell
""" """
self.wrapped = wrapped self.wrapped = wrapped
# Helps identify this solid through the use of an ID # Helps identify this solid through the use of an ID
self.label = "" self.label = ""
@classmethod @classmethod
@ -679,13 +686,14 @@ class Solid(Shape):
""" """
a single solid a single solid
""" """
def __init__(self, obj): def __init__(self, obj):
""" """
A Solid A Solid
""" """
self.wrapped = obj self.wrapped = obj
# Helps identify this solid through the use of an ID # Helps identify this solid through the use of an ID
self.label = "" self.label = ""
@classmethod @classmethod
@ -817,11 +825,11 @@ class Solid(Shape):
rs = FreeCADPart.makeRuledSurface(w1, w2) rs = FreeCADPart.makeRuledSurface(w1, w2)
sides.append(rs) sides.append(rs)
#make faces for the top and bottom # make faces for the top and bottom
startFace = FreeCADPart.Face(startWires) startFace = FreeCADPart.Face(startWires)
endFace = FreeCADPart.Face(endWires) endFace = FreeCADPart.Face(endWires)
#collect all the faces from the sides # collect all the faces from the sides
faceList = [startFace] faceList = [startFace]
for s in sides: for s in sides:
faceList.extend(s.Faces) faceList.extend(s.Faces)
@ -858,9 +866,9 @@ class Solid(Shape):
# one would think that fusing faces into a compound and then extruding would work, # one would think that fusing faces into a compound and then extruding would work,
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc), # but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
# but then cutting it from the main solid fails with BRep_NotDone. # but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work # the work around is to extrude each and then join the resulting solids, which seems to work
#FreeCAD allows this in one operation, but others might not # FreeCAD allows this in one operation, but others might not
freeCADWires = [outerWire.wrapped] freeCADWires = [outerWire.wrapped]
for w in innerWires: for w in innerWires:
freeCADWires.append(w.wrapped) freeCADWires.append(w.wrapped)
@ -906,10 +914,10 @@ class Solid(Shape):
rotateCenter = FreeCAD.Base.Vector(axisStart) rotateCenter = FreeCAD.Base.Vector(axisStart)
rotateAxis = FreeCAD.Base.Vector(axisEnd) rotateAxis = FreeCAD.Base.Vector(axisEnd)
#Convert our axis end vector into to something FreeCAD will understand (an axis specification vector) # Convert our axis end vector into to something FreeCAD will understand (an axis specification vector)
rotateAxis = rotateCenter.sub(rotateAxis) rotateAxis = rotateCenter.sub(rotateAxis)
#FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation # FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation
result = f.revolve(rotateCenter, rotateAxis, angleDegrees) result = f.revolve(rotateCenter, rotateAxis, angleDegrees)
return Shape.cast(result) return Shape.cast(result)
@ -1012,7 +1020,7 @@ class Compound(Shape):
""" """
self.wrapped = obj self.wrapped = obj
# Helps identify this solid through the use of an ID # Helps identify this solid through the use of an ID
self.label = "" self.label = ""
def Center(self): def Center(self):

View File

@ -2,8 +2,13 @@ from OCC.Visualization import Tesselator
import cadquery import cadquery
import tempfile, os import tempfile
import cStringIO as StringIO import os
import sys
if sys.version_info.major == 2:
import cStringIO as StringIO
else:
import io as StringIO
from .shapes import Shape, Compound, TOLERANCE from .shapes import Shape, Compound, TOLERANCE
from .geom import BoundBox from .geom import BoundBox
@ -23,7 +28,8 @@ except ImportError:
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
DISCRETIZATION_TOLERANCE = 1e-3 DISCRETIZATION_TOLERANCE = 1e-3
DEFAULT_DIR = gp_Dir(-1.75,1.1,5) DEFAULT_DIR = gp_Dir(-1.75, 1.1, 5)
class ExportTypes: class ExportTypes:
STL = "STL" STL = "STL"
@ -44,7 +50,7 @@ def toString(shape, exportType, tolerance=0.1):
return s.getvalue() return s.getvalue()
def exportShape(shape,exportType,fileLike,tolerance=0.1): def exportShape(shape, exportType, fileLike, tolerance=0.1):
""" """
:param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery :param shape: the shape to export. it can be a shape object, or a cadquery object. If a cadquery
object, the first value is exported object, the first value is exported
@ -55,33 +61,31 @@ def exportShape(shape,exportType,fileLike,tolerance=0.1):
for closing the object for closing the object
""" """
def tessellate(shape): def tessellate(shape):
tess = Tesselator(shape.wrapped) tess = Tesselator(shape.wrapped)
tess.Compute(compute_edges=True, mesh_quality=tolerance) tess.Compute(compute_edges=True, mesh_quality=tolerance)
return tess return tess
if isinstance(shape, cadquery.CQ):
if isinstance(shape,cadquery.CQ):
shape = shape.val() shape = shape.val()
if exportType == ExportTypes.TJS: if exportType == ExportTypes.TJS:
tess = tessellate(shape) tess = tessellate(shape)
mesher = JsonMesh() mesher = JsonMesh()
#add vertices # add vertices
for i_vert in range(tess.ObjGetVertexCount()): for i_vert in range(tess.ObjGetVertexCount()):
v = tess.GetVertex(i_vert) v = tess.GetVertex(i_vert)
mesher.addVertex(*v) mesher.addVertex(*v)
#add triangles # add triangles
for i_tr in range(tess.ObjGetTriangleCount()): for i_tr in range(tess.ObjGetTriangleCount()):
t = tess.GetTriangleIndex(i_tr) t = tess.GetTriangleIndex(i_tr)
mesher.addTriangleFace(*t) mesher.addTriangleFace(*t)
fileLike.write(mesher.toJson()) fileLike.write(mesher.toJson())
elif exportType == ExportTypes.SVG: elif exportType == ExportTypes.SVG:
fileLike.write(getSVG(shape)) fileLike.write(getSVG(shape))
elif exportType == ExportTypes.AMF: elif exportType == ExportTypes.AMF:
@ -90,11 +94,11 @@ def exportShape(shape,exportType,fileLike,tolerance=0.1):
aw.writeAmf(fileLike) aw.writeAmf(fileLike)
else: else:
#all these types required writing to a file and then # all these types required writing to a file and then
#re-reading. this is due to the fact that FreeCAD writes these # re-reading. this is due to the fact that FreeCAD writes these
(h, outFileName) = tempfile.mkstemp() (h, outFileName) = tempfile.mkstemp()
#weird, but we need to close this file. the next step is going to write to # weird, but we need to close this file. the next step is going to write to
#it from c code, so it needs to be closed. # it from c code, so it needs to be closed.
os.close(h) os.close(h)
if exportType == ExportTypes.STEP: if exportType == ExportTypes.STEP:
@ -107,13 +111,14 @@ def exportShape(shape,exportType,fileLike,tolerance=0.1):
res = readAndDeleteFile(outFileName) res = readAndDeleteFile(outFileName)
fileLike.write(res) fileLike.write(res)
def readAndDeleteFile(fileName): def readAndDeleteFile(fileName):
""" """
read data from file provided, and delete it when done read data from file provided, and delete it when done
return the contents as a string return the contents as a string
""" """
res = "" res = ""
with open(fileName,'r') as f: with open(fileName, 'r') as f:
res = f.read() res = f.read()
os.remove(fileName) os.remove(fileName)
@ -126,16 +131,16 @@ def guessUnitOfMeasure(shape):
""" """
bb = BoundBox._fromTopoDS(shape.wrapped) bb = BoundBox._fromTopoDS(shape.wrapped)
dimList = [ bb.xlen, bb.ylen,bb.zlen ] dimList = [bb.xlen, bb.ylen, bb.zlen]
#no real part would likely be bigger than 10 inches on any side # no real part would likely be bigger than 10 inches on any side
if max(dimList) > 10: if max(dimList) > 10:
return UNITS.MM return UNITS.MM
#no real part would likely be smaller than 0.1 mm on all dimensions # no real part would likely be smaller than 0.1 mm on all dimensions
if min(dimList) < 0.1: if min(dimList) < 0.1:
return UNITS.IN return UNITS.IN
#no real part would have the sum of its dimensions less than about 5mm # no real part would have the sum of its dimensions less than about 5mm
if sum(dimList) < 10: if sum(dimList) < 10:
return UNITS.IN return UNITS.IN
@ -143,109 +148,113 @@ def guessUnitOfMeasure(shape):
class AmfWriter(object): class AmfWriter(object):
def __init__(self,tessellation): def __init__(self, tessellation):
self.units = "mm" self.units = "mm"
self.tessellation = tessellation self.tessellation = tessellation
def writeAmf(self,outFile): def writeAmf(self, outFile):
amf = ET.Element('amf',units=self.units) amf = ET.Element('amf', units=self.units)
#TODO: if result is a compound, we need to loop through them # TODO: if result is a compound, we need to loop through them
object = ET.SubElement(amf,'object',id="0") object = ET.SubElement(amf, 'object', id="0")
mesh = ET.SubElement(object,'mesh') mesh = ET.SubElement(object, 'mesh')
vertices = ET.SubElement(mesh,'vertices') vertices = ET.SubElement(mesh, 'vertices')
volume = ET.SubElement(mesh,'volume') volume = ET.SubElement(mesh, 'volume')
#add vertices # add vertices
for i_vert in range(self.tessellation.ObjGetVertexCount()): for i_vert in range(self.tessellation.ObjGetVertexCount()):
v = self.tessellation.GetVertex(i_vert) v = self.tessellation.GetVertex(i_vert)
vtx = ET.SubElement(vertices,'vertex') vtx = ET.SubElement(vertices, 'vertex')
coord = ET.SubElement(vtx,'coordinates') coord = ET.SubElement(vtx, 'coordinates')
x = ET.SubElement(coord,'x') x = ET.SubElement(coord, 'x')
x.text = str(v[0]) x.text = str(v[0])
y = ET.SubElement(coord,'y') y = ET.SubElement(coord, 'y')
y.text = str(v[1]) y.text = str(v[1])
z = ET.SubElement(coord,'z') z = ET.SubElement(coord, 'z')
z.text = str(v[2]) z.text = str(v[2])
#add triangles # add triangles
for i_tr in range(self.tessellation.ObjGetTriangleCount()): for i_tr in range(self.tessellation.ObjGetTriangleCount()):
t = self.tessellation.GetTriangleIndex(i_tr) t = self.tessellation.GetTriangleIndex(i_tr)
triangle = ET.SubElement(volume,'triangle') triangle = ET.SubElement(volume, 'triangle')
v1 = ET.SubElement(triangle,'v1') v1 = ET.SubElement(triangle, 'v1')
v1.text = str(t[0]) v1.text = str(t[0])
v2 = ET.SubElement(triangle,'v2') v2 = ET.SubElement(triangle, 'v2')
v2.text = str(t[1]) v2.text = str(t[1])
v3 = ET.SubElement(triangle,'v3') v3 = ET.SubElement(triangle, 'v3')
v3.text = str(t[2]) v3.text = str(t[2])
ET.ElementTree(amf).write(outFile, encoding='ISO-8859-1')
ET.ElementTree(amf).write(outFile,encoding='ISO-8859-1')
""" """
Objects that represent Objects that represent
three.js JSON object notation three.js JSON object notation
https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0 https://github.com/mrdoob/three.js/wiki/JSON-Model-format-3.0
""" """
class JsonMesh(object): class JsonMesh(object):
def __init__(self): def __init__(self):
self.vertices = []; self.vertices = []
self.faces = []; self.faces = []
self.nVertices = 0; self.nVertices = 0
self.nFaces = 0; self.nFaces = 0
def addVertex(self,x,y,z): def addVertex(self, x, y, z):
self.nVertices += 1; self.nVertices += 1
self.vertices.extend([x,y,z]); self.vertices.extend([x, y, z])
#add triangle composed of the three provided vertex indices # add triangle composed of the three provided vertex indices
def addTriangleFace(self, i,j,k): def addTriangleFace(self, i, j, k):
#first position means justa simple triangle # first position means justa simple triangle
self.nFaces += 1; self.nFaces += 1
self.faces.extend([0,int(i),int(j),int(k)]); self.faces.extend([0, int(i), int(j), int(k)])
""" """
Get a json model from this model. Get a json model from this model.
For now we'll forget about colors, vertex normals, and all that stuff For now we'll forget about colors, vertex normals, and all that stuff
""" """
def toJson(self): def toJson(self):
return JSON_TEMPLATE % { return JSON_TEMPLATE % {
'vertices' : str(self.vertices), 'vertices': str(self.vertices),
'faces' : str(self.faces), 'faces': str(self.faces),
'nVertices': self.nVertices, 'nVertices': self.nVertices,
'nFaces' : self.nFaces 'nFaces': self.nFaces
}; };
def makeSVGedge(e): def makeSVGedge(e):
""" """
""" """
cs = StringIO.StringIO() cs = StringIO.StringIO()
curve = e._geomAdaptor() #adapt the edge into curve curve = e._geomAdaptor() # adapt the edge into curve
start = curve.FirstParameter() start = curve.FirstParameter()
end = curve.LastParameter() end = curve.LastParameter()
points = GCPnts_QuasiUniformDeflection(curve, points = GCPnts_QuasiUniformDeflection(curve,
DISCRETIZATION_TOLERANCE, DISCRETIZATION_TOLERANCE,
start, start,
end) end)
if points.IsDone(): if points.IsDone():
point_it = (points.Value(i+1) for i in \ point_it = (points.Value(i + 1) for i in
range(points.NbPoints())) range(points.NbPoints()))
p = point_it.next() p = next(point_it)
cs.write('M{},{} '.format(p.X(),p.Y())) cs.write('M{},{} '.format(p.X(), p.Y()))
for p in point_it: for p in point_it:
cs.write('L{},{} '.format(p.X(),p.Y())) cs.write('L{},{} '.format(p.X(), p.Y()))
return cs.getvalue() return cs.getvalue()
def getPaths(visibleShapes, hiddenShapes): def getPaths(visibleShapes, hiddenShapes):
""" """
@ -257,56 +266,55 @@ def getPaths(visibleShapes, hiddenShapes):
for s in visibleShapes: for s in visibleShapes:
for e in s.Edges(): for e in s.Edges():
visiblePaths.append(makeSVGedge(e)) visiblePaths.append(makeSVGedge(e))
for s in hiddenShapes: for s in hiddenShapes:
for e in s.Edges(): for e in s.Edges():
hiddenPaths.append(makeSVGedge(e)) hiddenPaths.append(makeSVGedge(e))
return (hiddenPaths,visiblePaths) return (hiddenPaths, visiblePaths)
def getSVG(shape, opts=None):
def getSVG(shape,opts=None):
""" """
Export a shape to SVG Export a shape to SVG
""" """
d = {'width':800,'height':240,'marginLeft':200,'marginTop':20} d = {'width': 800, 'height': 240, 'marginLeft': 200, 'marginTop': 20}
if opts: if opts:
d.update(opts) d.update(opts)
#need to guess the scale and the coordinate center # need to guess the scale and the coordinate center
uom = guessUnitOfMeasure(shape) uom = guessUnitOfMeasure(shape)
width=float(d['width']) width = float(d['width'])
height=float(d['height']) height = float(d['height'])
marginLeft=float(d['marginLeft']) marginLeft = float(d['marginLeft'])
marginTop=float(d['marginTop']) marginTop = float(d['marginTop'])
hlr = HLRBRep_Algo() hlr = HLRBRep_Algo()
hlr.Add(shape.wrapped) hlr.Add(shape.wrapped)
projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(),
DEFAULT_DIR) DEFAULT_DIR)
) )
hlr.Projector(projector) hlr.Projector(projector)
hlr.Update() hlr.Update()
hlr.Hide() hlr.Hide()
hlr_shapes = HLRBRep_HLRToShape(hlr.GetHandle()) hlr_shapes = HLRBRep_HLRToShape(hlr.GetHandle())
visible = [] visible = []
visible_sharp_edges = hlr_shapes.VCompound() visible_sharp_edges = hlr_shapes.VCompound()
if not visible_sharp_edges.IsNull(): if not visible_sharp_edges.IsNull():
visible.append(visible_sharp_edges) visible.append(visible_sharp_edges)
visible_smooth_edges = hlr_shapes.Rg1LineVCompound() visible_smooth_edges = hlr_shapes.Rg1LineVCompound()
if not visible_smooth_edges.IsNull(): if not visible_smooth_edges.IsNull():
visible.append(visible_smooth_edges) visible.append(visible_smooth_edges)
visible_contour_edges = hlr_shapes.OutLineVCompound() visible_contour_edges = hlr_shapes.OutLineVCompound()
if not visible_contour_edges.IsNull(): if not visible_contour_edges.IsNull():
visible.append(visible_contour_edges) visible.append(visible_contour_edges)
@ -316,31 +324,34 @@ def getSVG(shape,opts=None):
hidden_sharp_edges = hlr_shapes.HCompound() hidden_sharp_edges = hlr_shapes.HCompound()
if not hidden_sharp_edges.IsNull(): if not hidden_sharp_edges.IsNull():
hidden.append(hidden_sharp_edges) hidden.append(hidden_sharp_edges)
hidden_contour_edges = hlr_shapes.OutLineHCompound() hidden_contour_edges = hlr_shapes.OutLineHCompound()
if not hidden_contour_edges.IsNull(): if not hidden_contour_edges.IsNull():
hidden.append(hidden_contour_edges) hidden.append(hidden_contour_edges)
#Fix the underlying geometry - otherwise we will get segfaults # Fix the underlying geometry - otherwise we will get segfaults
for el in visible: breplib.BuildCurves3d(el,TOLERANCE) for el in visible:
for el in hidden: breplib.BuildCurves3d(el,TOLERANCE) breplib.BuildCurves3d(el, TOLERANCE)
for el in hidden:
breplib.BuildCurves3d(el, TOLERANCE)
#convert to native CQ objects # convert to native CQ objects
visible = map(Shape,visible) visible = list(map(Shape, visible))
hidden = map(Shape,hidden) hidden = list(map(Shape, hidden))
(hiddenPaths,visiblePaths) = getPaths(visible, (hiddenPaths, visiblePaths) = getPaths(visible,
hidden) hidden)
#get bounding box -- these are all in 2-d space # get bounding box -- these are all in 2-d space
bb = Compound.makeCompound(hidden+visible).BoundingBox() bb = Compound.makeCompound(hidden + visible).BoundingBox()
#width pixels for x, height pixesl for y # width pixels for x, height pixesl for y
unitScale = min( width / bb.xlen * 0.75 , height / bb.ylen * 0.75 ) unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75)
#compute amount to translate-- move the top left into view # compute amount to translate-- move the top left into view
(xTranslate,yTranslate) = ( (0 - bb.xmin) + marginLeft/unitScale ,(0- bb.ymax) - marginTop/unitScale) (xTranslate, yTranslate) = ((0 - bb.xmin) + marginLeft /
unitScale, (0 - bb.ymax) - marginTop / unitScale)
#compute paths ( again -- had to strip out freecad crap ) # compute paths ( again -- had to strip out freecad crap )
hiddenContent = "" hiddenContent = ""
for p in hiddenPaths: for p in hiddenPaths:
hiddenContent += PATHTEMPLATE % p hiddenContent += PATHTEMPLATE % p
@ -349,21 +360,21 @@ def getSVG(shape,opts=None):
for p in visiblePaths: for p in visiblePaths:
visibleContent += PATHTEMPLATE % p visibleContent += PATHTEMPLATE % p
svg = SVG_TEMPLATE % ( svg = SVG_TEMPLATE % (
{ {
"unitScale" : str(unitScale), "unitScale": str(unitScale),
"strokeWidth" : str(1.0/unitScale), "strokeWidth": str(1.0 / unitScale),
"hiddenContent" : hiddenContent , "hiddenContent": hiddenContent,
"visibleContent" :visibleContent, "visibleContent": visibleContent,
"xTranslate" : str(xTranslate), "xTranslate": str(xTranslate),
"yTranslate" : str(yTranslate), "yTranslate": str(yTranslate),
"width" : str(width), "width": str(width),
"height" : str(height), "height": str(height),
"textboxY" :str(height - 30), "textboxY": str(height - 30),
"uom" : str(uom) "uom": str(uom)
} }
) )
#svg = SVG_TEMPLATE % ( # svg = SVG_TEMPLATE % (
# {"content": projectedContent} # {"content": projectedContent}
#) #)
return svg return svg
@ -377,13 +388,12 @@ def exportSVG(shape, fileName):
""" """
svg = getSVG(shape.val()) svg = getSVG(shape.val())
f = open(fileName,'w') f = open(fileName, 'w')
f.write(svg) f.write(svg)
f.close() f.close()
JSON_TEMPLATE = """\
JSON_TEMPLATE= """\
{ {
"metadata" : "metadata" :
{ {
@ -463,5 +473,4 @@ SVG_TEMPLATE = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
</svg> </svg>
""" """
PATHTEMPLATE="\t\t\t<path d=\"%s\" />\n" PATHTEMPLATE = "\t\t\t<path d=\"%s\" />\n"

View File

@ -3,7 +3,7 @@ import cadquery
from OCC.gp import gp_Vec, gp_Ax1, gp_Ax3, gp_Pnt, gp_Dir, gp_Trsf, gp, gp_XYZ from OCC.gp import gp_Vec, gp_Ax1, gp_Ax3, gp_Pnt, gp_Dir, gp_Trsf, gp, gp_XYZ
from OCC.Bnd import Bnd_Box from OCC.Bnd import Bnd_Box
from OCC.BRepBndLib import brepbndlib_Add # brepbndlib_AddOptimal from OCC.BRepBndLib import brepbndlib_Add # brepbndlib_AddOptimal
from OCC.BRepMesh import BRepMesh_IncrementalMesh from OCC.BRepMesh import BRepMesh_IncrementalMesh
TOL = 1e-2 TOL = 1e-2
@ -21,6 +21,7 @@ class Vector(object):
* a 3-tuple * a 3-tuple
* three float values, x, y, and z * three float values, x, y, and z
""" """
def __init__(self, *args): def __init__(self, *args):
if len(args) == 3: if len(args) == 3:
fV = gp_Vec(*args) fV = gp_Vec(*args)
@ -40,7 +41,7 @@ class Vector(object):
else: else:
fV = args[0] fV = args[0]
elif len(args) == 0: elif len(args) == 0:
fV = gp_Vec(0,0,0) fV = gp_Vec(0, 0, 0)
else: else:
raise ValueError("Expected three floats, OCC Geom_, or 3-tuple") raise ValueError("Expected three floats, OCC Geom_, or 3-tuple")
@ -104,29 +105,33 @@ class Vector(object):
return self.wrapped.Angle(v.wrapped) return self.wrapped.Angle(v.wrapped)
def distanceToLine(self): def distanceToLine(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def projectToLine(self): def projectToLine(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def distanceToPlane(self): def distanceToPlane(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def projectToPlane(self): def projectToPlane(self):
raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") raise NotImplementedError(
"Have not needed this yet, but FreeCAD supports it!")
def __add__(self, v): def __add__(self, v):
return self.add(v) return self.add(v)
def __sub__(self, v): def __sub__(self, v):
return self.sub(v) return self.sub(v)
def __repr__(self): def __repr__(self):
return 'Vector: ' + str((self.x,self.y,self.z)) return 'Vector: ' + str((self.x, self.y, self.z))
def __str__(self): def __str__(self):
return 'Vector: ' + str((self.x,self.y,self.z)) return 'Vector: ' + str((self.x, self.y, self.z))
def __eq__(self, other): def __eq__(self, other):
return self.wrapped == other.wrapped return self.wrapped == other.wrapped
''' '''
@ -134,21 +139,21 @@ class Vector(object):
def __ne__(self, other): def __ne__(self, other):
return self.wrapped.__ne__(other) return self.wrapped.__ne__(other)
''' '''
def toPnt(self): def toPnt(self):
return gp_Pnt(self.wrapped.XYZ()) return gp_Pnt(self.wrapped.XYZ())
def toDir(self): def toDir(self):
return gp_Dir(self.wrapped.XYZ()) return gp_Dir(self.wrapped.XYZ())
def transform(self,T): def transform(self, T):
#to gp_Pnt to obey cq transformation convention (in OCC vectors do not translate) # to gp_Pnt to obey cq transformation convention (in OCC vectors do not translate)
pnt = self.toPnt() pnt = self.toPnt()
pnt_t = pnt.Transformed(T.wrapped) pnt_t = pnt.Transformed(T.wrapped)
return Vector(gp_Vec(pnt_t.XYZ())) return Vector(gp_Vec(pnt_t.XYZ()))
@ -157,6 +162,7 @@ class Matrix:
Used to move geometry in space. Used to move geometry in space.
""" """
def __init__(self, matrix=None): def __init__(self, matrix=None):
if matrix is None: if matrix is None:
self.wrapped = gp_Trsf() self.wrapped = gp_Trsf()
@ -170,19 +176,19 @@ class Matrix:
def rotateY(self, angle): def rotateY(self, angle):
self._rotate(gp.OY(), self._rotate(gp.OY(),
angle) angle)
def rotateZ(self, angle): def rotateZ(self, angle):
self._rotate(gp.OZ(), self._rotate(gp.OZ(),
angle) angle)
def _rotate(self,direction,angle): def _rotate(self, direction, angle):
new = gp_Trsf() new = gp_Trsf()
new.SetRotation(direction, new.SetRotation(direction,
angle) angle)
self.wrapped = self.wrapped * new self.wrapped = self.wrapped * new
def inverse(self): def inverse(self):
return Matrix(self.wrapped.Invert()) return Matrix(self.wrapped.Invert())
@ -199,7 +205,7 @@ class Plane(object):
Frequently, it is not necessary to create work planes, as they can be Frequently, it is not necessary to create work planes, as they can be
created automatically from faces. created automatically from faces.
""" """
@classmethod @classmethod
def named(cls, stdName, origin=(0, 0, 0)): def named(cls, stdName, origin=(0, 0, 0)):
"""Create a predefined Plane based on the conventional names. """Create a predefined Plane based on the conventional names.
@ -250,7 +256,7 @@ class Plane(object):
return namedPlanes[stdName] return namedPlanes[stdName]
except KeyError: except KeyError:
raise ValueError('Supported names are {}'.format( raise ValueError('Supported names are {}'.format(
namedPlanes.keys())) list(namedPlanes.keys())))
@classmethod @classmethod
def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
@ -340,11 +346,11 @@ class Plane(object):
zDir = Vector(normal) zDir = Vector(normal)
if (zDir.Length == 0.0): if (zDir.Length == 0.0):
raise ValueError('normal should be non null') raise ValueError('normal should be non null')
xDir = Vector(xDir) xDir = Vector(xDir)
if (xDir.Length == 0.0): if (xDir.Length == 0.0):
raise ValueError('xDir should be non null') raise ValueError('xDir should be non null')
self.zDir = zDir.normalized() self.zDir = zDir.normalized()
self._setPlaneDir(xDir) self._setPlaneDir(xDir)
self.origin = origin self.origin = origin
@ -353,6 +359,7 @@ class Plane(object):
def origin(self): def origin(self):
return self._origin return self._origin
# TODO is this property rly needed -- why not handle this in the constructor # TODO is this property rly needed -- why not handle this in the constructor
@origin.setter @origin.setter
def origin(self, value): def origin(self, value):
self._origin = Vector(value) self._origin = Vector(value)
@ -404,9 +411,9 @@ class Plane(object):
* Discretizing points along each curve to provide a more reliable * Discretizing points along each curve to provide a more reliable
test. test.
""" """
pass pass
''' '''
# TODO: also use a set of points along the wire to test as well. # 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 # TODO: would it be more efficient to create objects in the local
@ -425,7 +432,7 @@ class Plane(object):
# know if one is inside the other # know if one is inside the other
return bb == BoundBox.findOutsideBox2D(bb, tb) return bb == BoundBox.findOutsideBox2D(bb, tb)
''' '''
def toLocalCoords(self, obj): def toLocalCoords(self, obj):
"""Project the provided coordinates onto this plane """Project the provided coordinates onto this plane
@ -511,7 +518,7 @@ class Plane(object):
# - then rotate about x # - then rotate about x
# - then transform back to global coordinates. # - then transform back to global coordinates.
#TODO why is it here? # TODO why is it here?
raise NotImplementedError raise NotImplementedError
@ -542,14 +549,14 @@ class Plane(object):
resultWires.append(cadquery.Shape.cast(mirroredWire)) resultWires.append(cadquery.Shape.cast(mirroredWire))
return resultWires''' return resultWires'''
def mirrorInPlane(self, listOfShapes, axis = 'X'): def mirrorInPlane(self, listOfShapes, axis='X'):
local_coord_system = gp_Ax3(self.origin.toPnt(), local_coord_system = gp_Ax3(self.origin.toPnt(),
self.zDir.toDir(), self.zDir.toDir(),
self.xDir.toDir()) self.xDir.toDir())
T = gp_Trsf() T = gp_Trsf()
if axis == 'X': if axis == 'X':
T.SetMirror(gp_Ax1(self.origin.toPnt(), T.SetMirror(gp_Ax1(self.origin.toPnt(),
local_coord_system.XDirection())) local_coord_system.XDirection()))
@ -558,16 +565,16 @@ class Plane(object):
local_coord_system.YDirection())) local_coord_system.YDirection()))
else: else:
raise NotImplementedError raise NotImplementedError
resultWires = [] resultWires = []
for w in listOfShapes: for w in listOfShapes:
mirrored = w.transformShape(Matrix(T)) mirrored = w.transformShape(Matrix(T))
#attemp stitching of the wires # attemp stitching of the wires
resultWires.append(mirrored) resultWires.append(mirrored)
return resultWires return resultWires
def _setPlaneDir(self, xDir): def _setPlaneDir(self, xDir):
"""Set the vectors parallel to the plane, i.e. xDir and yDir""" """Set the vectors parallel to the plane, i.e. xDir and yDir"""
xDir = Vector(xDir) xDir = Vector(xDir)
@ -586,32 +593,32 @@ class Plane(object):
# the double-inverting is strange, and I don't understand it. # the double-inverting is strange, and I don't understand it.
forward = Matrix() forward = Matrix()
inverse = Matrix() inverse = Matrix()
global_coord_system = gp_Ax3() global_coord_system = gp_Ax3()
local_coord_system = gp_Ax3(gp_Pnt(*self.origin.toTuple()), local_coord_system = gp_Ax3(gp_Pnt(*self.origin.toTuple()),
gp_Dir(*self.zDir.toTuple()), gp_Dir(*self.zDir.toTuple()),
gp_Dir(*self.xDir.toTuple()) gp_Dir(*self.xDir.toTuple())
) )
forward.wrapped.SetTransformation(global_coord_system, forward.wrapped.SetTransformation(global_coord_system,
local_coord_system) local_coord_system)
inverse.wrapped.SetTransformation(local_coord_system, inverse.wrapped.SetTransformation(local_coord_system,
global_coord_system) global_coord_system)
#TODO verify if this is OK # TODO verify if this is OK
self.lcs = local_coord_system self.lcs = local_coord_system
self.rG = inverse self.rG = inverse
self.fG = forward self.fG = forward
class BoundBox(object): class BoundBox(object):
"""A BoundingBox for an object or set of objects. Wraps the OCC one""" """A BoundingBox for an object or set of objects. Wraps the OCC one"""
def __init__(self, bb): def __init__(self, bb):
self.wrapped = bb self.wrapped = bb
XMin, YMin, ZMin, XMax, YMax, ZMax = bb.Get() XMin, YMin, ZMin, XMax, YMax, ZMax = bb.Get()
self.xmin = XMin self.xmin = XMin
self.xmax = XMax self.xmax = XMax
self.xlen = XMax - XMin self.xlen = XMax - XMin
@ -621,11 +628,11 @@ class BoundBox(object):
self.zmin = ZMin self.zmin = ZMin
self.zmax = ZMax self.zmax = ZMax
self.zlen = ZMax - ZMin self.zlen = ZMax - ZMin
self.center = Vector((XMax+XMin)/2, self.center = Vector((XMax + XMin) / 2,
(YMax+YMin)/2, (YMax + YMin) / 2,
(ZMax+ZMin)/2) (ZMax + ZMin) / 2)
self.DiagonalLength = self.wrapped.SquareExtent()**0.5 self.DiagonalLength = self.wrapped.SquareExtent()**0.5
def add(self, obj, tol=1e-8): def add(self, obj, tol=1e-8):
@ -639,11 +646,11 @@ class BoundBox(object):
This bounding box is not changed. This bounding box is not changed.
""" """
tmp = Bnd_Box() tmp = Bnd_Box()
tmp.SetGap(tol) tmp.SetGap(tol)
tmp.Add(self.wrapped) tmp.Add(self.wrapped)
if isinstance(obj, tuple): if isinstance(obj, tuple):
tmp.Update(*obj) tmp.Update(*obj)
elif isinstance(obj, Vector): elif isinstance(obj, Vector):
@ -664,23 +671,23 @@ class BoundBox(object):
doesn't work correctly plus, there was all kinds of rounding error in doesn't work correctly plus, there was all kinds of rounding error in
the built-in implementation i do not understand. the built-in implementation i do not understand.
""" """
if (bb1.XMin < bb2.XMin and if (bb1.XMin < bb2.XMin and
bb1.XMax > bb2.XMax and bb1.XMax > bb2.XMax and
bb1.YMin < bb2.YMin and bb1.YMin < bb2.YMin and
bb1.YMax > bb2.YMax): bb1.YMax > bb2.YMax):
return bb1 return bb1
if (bb2.XMin < bb1.XMin and if (bb2.XMin < bb1.XMin and
bb2.XMax > bb1.XMax and bb2.XMax > bb1.XMax and
bb2.YMin < bb1.YMin and bb2.YMin < bb1.YMin and
bb2.YMax > bb1.YMax): bb2.YMax > bb1.YMax):
return bb2 return bb2
return None return None
@classmethod @classmethod
def _fromTopoDS(cls,shape,tol=TOL,optimal=False): def _fromTopoDS(cls, shape, tol=TOL, optimal=False):
''' '''
Constructs a bounnding box from a TopoDS_Shape Constructs a bounnding box from a TopoDS_Shape
''' '''
@ -688,14 +695,15 @@ class BoundBox(object):
bbox.SetGap(tol) bbox.SetGap(tol)
if optimal: if optimal:
raise NotImplementedError raise NotImplementedError
#brepbndlib_AddOptimal(shape, bbox) #this is 'exact' but expensive - not yet wrapped by PythonOCC # brepbndlib_AddOptimal(shape, bbox) #this is 'exact' but expensive - not yet wrapped by PythonOCC
else: else:
mesh = BRepMesh_IncrementalMesh(shape,TOL,True) mesh = BRepMesh_IncrementalMesh(shape, TOL, True)
mesh.Perform() mesh.Perform()
brepbndlib_Add(shape, bbox, True) #this is adds +margin but is faster # this is adds +margin but is faster
brepbndlib_Add(shape, bbox, True)
return cls(bbox) return cls(bbox)
def isInside(self, anotherBox): def isInside(self, anotherBox):
"""Is the provided bounding box inside this one?""" """Is the provided bounding box inside this one?"""
return not anotherBox.wrapped.IsOut(self.wrapped) return not anotherBox.wrapped.IsOut(self.wrapped)

View File

@ -8,10 +8,12 @@ import urllib as urlreader
import tempfile import tempfile
from OCC.STEPControl import STEPControl_Reader from OCC.STEPControl import STEPControl_Reader
class ImportTypes: class ImportTypes:
STEP = "STEP" STEP = "STEP"
class UNITS: class UNITS:
MM = "mm" MM = "mm"
IN = "in" IN = "in"
@ -24,18 +26,18 @@ def importShape(importType, fileName):
:param fileName: THe name of the file that we're importing :param fileName: THe name of the file that we're importing
""" """
#Check to see what type of file we're working with # Check to see what type of file we're working with
if importType == ImportTypes.STEP: if importType == ImportTypes.STEP:
return importStep(fileName) return importStep(fileName)
#Loads a STEP file into a CQ.Workplane object # Loads a STEP file into a CQ.Workplane object
def importStep(fileName): def importStep(fileName):
""" """
Accepts a file name and loads the STEP file into a cadquery shape Accepts a file name and loads the STEP file into a cadquery shape
:param fileName: The path and name of the STEP file to be imported :param fileName: The path and name of the STEP file to be imported
""" """
#Now read and return the shape # Now read and return the shape
try: try:
reader = STEPControl_Reader() reader = STEPControl_Reader()
reader.ReadFile(fileName) reader.ReadFile(fileName)
@ -43,27 +45,30 @@ def importStep(fileName):
occ_shapes = [] occ_shapes = []
for i in range(reader.NbShapes()): for i in range(reader.NbShapes()):
occ_shapes.append(reader.Shape(i+1)) occ_shapes.append(reader.Shape(i + 1))
#Make sure that we extract all the solids # Make sure that we extract all the solids
solids = [] solids = []
for shape in occ_shapes: for shape in occ_shapes:
solids.append(Shape.cast(shape)) solids.append(Shape.cast(shape))
return cadquery.Workplane("XY").newObject(solids) return cadquery.Workplane("XY").newObject(solids)
except: except:
raise ValueError("STEP File Could not be loaded") raise ValueError("STEP File Could not be loaded")
#Loads a STEP file from an URL into a CQ.Workplane object # Loads a STEP file from an URL into a CQ.Workplane object
def importStepFromURL(url):
#Now read and return the shape
def importStepFromURL(url):
# Now read and return the shape
try: try:
webFile = urlreader.urlopen(url) webFile = urlreader.urlopen(url)
tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False) tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False)
tempFile.write(webFile.read()) tempFile.write(webFile.read())
webFile.close() webFile.close()
tempFile.close() tempFile.close()
return importStep(tempFile.name) return importStep(tempFile.name)
except: except:
raise ValueError("STEP File from the URL: " + url + " Could not be loaded") raise ValueError("STEP File from the URL: " +
url + " Could not be loaded")

File diff suppressed because it is too large Load Diff

View File

@ -19,11 +19,12 @@
import re import re
import math import math
from cadquery import Vector,Edge,Vertex,Face,Solid,Shell,Compound from cadquery import Vector, Edge, Vertex, Face, Solid, Shell, Compound
from collections import defaultdict from collections import defaultdict
from pyparsing import Literal,Word,nums,Optional,Combine,oneOf,upcaseTokens,\ from pyparsing import Literal, Word, nums, Optional, Combine, oneOf, upcaseTokens,\
CaselessLiteral,Group,infixNotation,opAssoc,Forward,\ CaselessLiteral, Group, infixNotation, opAssoc, Forward,\
ZeroOrMore,Keyword ZeroOrMore, Keyword
from functools import reduce
class Selector(object): class Selector(object):
@ -32,7 +33,8 @@ class Selector(object):
Filters must provide a single method that filters objects. Filters must provide a single method that filters objects.
""" """
def filter(self,objectList):
def filter(self, objectList):
""" """
Filter the provided list Filter the provided list
:param objectList: list to filter :param objectList: list to filter
@ -56,6 +58,7 @@ class Selector(object):
def __neg__(self): def __neg__(self):
return InverseSelector(self) return InverseSelector(self)
class NearestToPointSelector(Selector): class NearestToPointSelector(Selector):
""" """
Selects object nearest the provided point. Selects object nearest the provided point.
@ -73,18 +76,21 @@ class NearestToPointSelector(Selector):
returns the vertex of the unit cube closest to the point x=0,y=1,z=0 returns the vertex of the unit cube closest to the point x=0,y=1,z=0
""" """
def __init__(self,pnt ):
def __init__(self, pnt):
self.pnt = pnt self.pnt = pnt
def filter(self,objectList):
def filter(self, objectList):
def dist(tShape): def dist(tShape):
return tShape.Center().sub(Vector(*self.pnt)).Length return tShape.Center().sub(Vector(*self.pnt)).Length
#if tShape.ShapeType == 'Vertex': # if tShape.ShapeType == 'Vertex':
# return tShape.Point.sub(toVector(self.pnt)).Length # return tShape.Point.sub(toVector(self.pnt)).Length
#else: # else:
# return tShape.CenterOfMass.sub(toVector(self.pnt)).Length # return tShape.CenterOfMass.sub(toVector(self.pnt)).Length
return [ min(objectList,key=dist) ] return [min(objectList, key=dist)]
class BoxSelector(Selector): class BoxSelector(Selector):
""" """
@ -100,6 +106,7 @@ class BoxSelector(Selector):
CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1)) CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1))
""" """
def __init__(self, point0, point1, boundingbox=False): def __init__(self, point0, point1, boundingbox=False):
self.p0 = Vector(*point0) self.p0 = Vector(*point0)
self.p1 = Vector(*point1) self.p1 = Vector(*point1)
@ -130,20 +137,22 @@ class BoxSelector(Selector):
return result return result
class BaseDirSelector(Selector): class BaseDirSelector(Selector):
""" """
A selector that handles selection on the basis of a single A selector that handles selection on the basis of a single
direction vector direction vector
""" """
def __init__(self,vector,tolerance=0.0001 ):
def __init__(self, vector, tolerance=0.0001):
self.direction = vector self.direction = vector
self.TOLERANCE = tolerance self.TOLERANCE = tolerance
def test(self,vec): def test(self, vec):
"Test a specified vector. Subclasses override to provide other implementations" "Test a specified vector. Subclasses override to provide other implementations"
return True return True
def filter(self,objectList): def filter(self, objectList):
""" """
There are lots of kinds of filters, but There are lots of kinds of filters, but
for planes they are always based on the normal of the plane, for planes they are always based on the normal of the plane,
@ -151,7 +160,7 @@ class BaseDirSelector(Selector):
""" """
r = [] r = []
for o in objectList: for o in objectList:
#no really good way to avoid a switch here, edges and faces are simply different! # no really good way to avoid a switch here, edges and faces are simply different!
if type(o) == Face: 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 # a face is only parallell to a direction if it is a plane, and its normal is parallel to the dir
@ -160,13 +169,14 @@ class BaseDirSelector(Selector):
if self.test(normal): if self.test(normal):
r.append(o) r.append(o)
elif type(o) == Edge and (o.geomType() == 'LINE' or o.geomType() == 'PLANE'): elif type(o) == Edge and (o.geomType() == 'LINE' or o.geomType() == 'PLANE'):
#an edge is parallel to a direction if its underlying geometry is plane or line # an edge is parallel to a direction if its underlying geometry is plane or line
tangent = o.tangentAt(None) tangent = o.tangentAt(None)
if self.test(tangent): if self.test(tangent):
r.append(o) r.append(o)
return r return r
class ParallelDirSelector(BaseDirSelector): class ParallelDirSelector(BaseDirSelector):
""" """
Selects objects parallel with the provided direction Selects objects parallel with the provided direction
@ -187,9 +197,10 @@ class ParallelDirSelector(BaseDirSelector):
CQ(aCube).faces("|Z") CQ(aCube).faces("|Z")
""" """
def test(self,vec): def test(self, vec):
return self.direction.cross(vec).Length < self.TOLERANCE return self.direction.cross(vec).Length < self.TOLERANCE
class DirectionSelector(BaseDirSelector): class DirectionSelector(BaseDirSelector):
""" """
Selects objects aligned with the provided direction Selects objects aligned with the provided direction
@ -210,9 +221,10 @@ class DirectionSelector(BaseDirSelector):
CQ(aCube).faces("+Z") CQ(aCube).faces("+Z")
""" """
def test(self,vec): def test(self, vec):
return abs(self.direction.getAngle(vec) < self.TOLERANCE) return abs(self.direction.getAngle(vec) < self.TOLERANCE)
class PerpendicularDirSelector(BaseDirSelector): class PerpendicularDirSelector(BaseDirSelector):
""" """
Selects objects perpendicular with the provided direction Selects objects perpendicular with the provided direction
@ -233,9 +245,10 @@ class PerpendicularDirSelector(BaseDirSelector):
CQ(aCube).faces("#Z") CQ(aCube).faces("#Z")
""" """
def test(self,vec): def test(self, vec):
angle = self.direction.getAngle(vec) angle = self.direction.getAngle(vec)
r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE ) r = (abs(angle) < self.TOLERANCE) or (
abs(angle - math.pi) < self.TOLERANCE)
return not r return not r
@ -259,16 +272,18 @@ class TypeSelector(Selector):
CQ(aCube).faces( "%PLANE" ) CQ(aCube).faces( "%PLANE" )
""" """
def __init__(self,typeString):
def __init__(self, typeString):
self.typeString = typeString.upper() self.typeString = typeString.upper()
def filter(self,objectList): def filter(self, objectList):
r = [] r = []
for o in objectList: for o in objectList:
if o.geomType() == self.typeString: if o.geomType() == self.typeString:
r.append(o) r.append(o)
return r return r
class DirectionMinMaxSelector(Selector): class DirectionMinMaxSelector(Selector):
""" """
Selects objects closest or farthest in the specified direction Selects objects closest or farthest in the specified direction
@ -291,32 +306,35 @@ class DirectionMinMaxSelector(Selector):
CQ(aCube).faces( ">Z" ) CQ(aCube).faces( ">Z" )
""" """
def __init__(self, vector, directionMax=True, tolerance=0.0001): def __init__(self, vector, directionMax=True, tolerance=0.0001):
self.vector = vector self.vector = vector
self.max = max self.max = max
self.directionMax = directionMax self.directionMax = directionMax
self.TOLERANCE = tolerance self.TOLERANCE = tolerance
def filter(self,objectList):
def filter(self, objectList):
def distance(tShape): def distance(tShape):
return tShape.Center().dot(self.vector) return tShape.Center().dot(self.vector)
# import OrderedDict # import OrderedDict
from collections import OrderedDict from collections import OrderedDict
#make and distance to object dict # make and distance to object dict
objectDict = {distance(el) : el for el in objectList} objectDict = {distance(el): el for el in objectList}
#transform it into an ordered dict # transform it into an ordered dict
objectDict = OrderedDict(sorted(objectDict.items(), objectDict = OrderedDict(sorted(list(objectDict.items()),
key=lambda x: x[0])) key=lambda x: x[0]))
# find out the max/min distance # find out the max/min distance
if self.directionMax: if self.directionMax:
d = objectDict.keys()[-1] d = list(objectDict.keys())[-1]
else: else:
d = objectDict.keys()[0] d = list(objectDict.keys())[0]
# return all objects at the max/min distance (within a tolerance) # return all objects at the max/min distance (within a tolerance)
return filter(lambda o: abs(d - distance(o)) < self.TOLERANCE, objectList) return [o for o in objectList if abs(d - distance(o)) < self.TOLERANCE]
class DirectionNthSelector(ParallelDirSelector): class DirectionNthSelector(ParallelDirSelector):
""" """
@ -327,41 +345,44 @@ class DirectionNthSelector(ParallelDirSelector):
Linear Edges Linear Edges
Planar Faces Planar Faces
""" """
def __init__(self, vector, n, directionMax=True, tolerance=0.0001): def __init__(self, vector, n, directionMax=True, tolerance=0.0001):
self.direction = vector self.direction = vector
self.max = max self.max = max
self.directionMax = directionMax self.directionMax = directionMax
self.TOLERANCE = tolerance self.TOLERANCE = tolerance
self.N = n self.N = n
def filter(self,objectList): def filter(self, objectList):
#select first the objects that are normal/parallel to a given dir # select first the objects that are normal/parallel to a given dir
objectList = super(DirectionNthSelector,self).filter(objectList) objectList = super(DirectionNthSelector, self).filter(objectList)
def distance(tShape): def distance(tShape):
return tShape.Center().dot(self.direction) return tShape.Center().dot(self.direction)
#calculate how many digits of precision do we need
digits = int(1/self.TOLERANCE)
#make a distance to object dict # calculate how many digits of precision do we need
#this is one to many mapping so I am using a default dict with list digits = int(1 / self.TOLERANCE)
# make a distance to object dict
# this is one to many mapping so I am using a default dict with list
objectDict = defaultdict(list) objectDict = defaultdict(list)
for el in objectList: for el in objectList:
objectDict[round(distance(el),digits)].append(el) objectDict[round(distance(el), digits)].append(el)
# choose the Nth unique rounded distance # choose the Nth unique rounded distance
nth_distance = sorted(objectDict.keys(), nth_distance = sorted(list(objectDict.keys()),
reverse=not self.directionMax)[self.N] reverse=not self.directionMax)[self.N]
# map back to original objects and return # map back to original objects and return
return objectDict[nth_distance] return objectDict[nth_distance]
class BinarySelector(Selector): class BinarySelector(Selector):
""" """
Base class for selectors that operates with two other Base class for selectors that operates with two other
selectors. Subclass must implement the :filterResults(): method. selectors. Subclass must implement the :filterResults(): method.
""" """
def __init__(self, left, right): def __init__(self, left, right):
self.left = left self.left = left
self.right = right self.right = right
@ -373,35 +394,43 @@ class BinarySelector(Selector):
def filterResults(self, r_left, r_right): def filterResults(self, r_left, r_right):
raise NotImplementedError raise NotImplementedError
class AndSelector(BinarySelector): class AndSelector(BinarySelector):
""" """
Intersection selector. Returns objects that is selected by both selectors. Intersection selector. Returns objects that is selected by both selectors.
""" """
def filterResults(self, r_left, r_right): def filterResults(self, r_left, r_right):
# return intersection of lists # return intersection of lists
return list(set(r_left) & set(r_right)) return list(set(r_left) & set(r_right))
class SumSelector(BinarySelector): class SumSelector(BinarySelector):
""" """
Union selector. Returns the sum of two selectors results. Union selector. Returns the sum of two selectors results.
""" """
def filterResults(self, r_left, r_right): def filterResults(self, r_left, r_right):
# return the union (no duplicates) of lists # return the union (no duplicates) of lists
return list(set(r_left + r_right)) return list(set(r_left + r_right))
class SubtractSelector(BinarySelector): class SubtractSelector(BinarySelector):
""" """
Difference selector. Substract results of a selector from another Difference selector. Substract results of a selector from another
selectors results. selectors results.
""" """
def filterResults(self, r_left, r_right): def filterResults(self, r_left, r_right):
return list(set(r_left) - set(r_right)) return list(set(r_left) - set(r_right))
class InverseSelector(Selector): class InverseSelector(Selector):
""" """
Inverts the selection of given selector. In other words, selects Inverts the selection of given selector. In other words, selects
all objects that is not selected by given selector. all objects that is not selected by given selector.
""" """
def __init__(self, selector): def __init__(self, selector):
self.selector = selector self.selector = selector
@ -414,189 +443,199 @@ def _makeGrammar():
""" """
Define the simple string selector grammar using PyParsing Define the simple string selector grammar using PyParsing
""" """
#float definition # float definition
point = Literal('.') point = Literal('.')
plusmin = Literal('+') | Literal('-') plusmin = Literal('+') | Literal('-')
number = Word(nums) number = Word(nums)
integer = Combine(Optional(plusmin) + number) integer = Combine(Optional(plusmin) + number)
floatn = Combine(integer + Optional(point + Optional(number))) floatn = Combine(integer + Optional(point + Optional(number)))
#vector definition # vector definition
lbracket = Literal('(') lbracket = Literal('(')
rbracket = Literal(')') rbracket = Literal(')')
comma = Literal(',') comma = Literal(',')
vector = Combine(lbracket + floatn('x') + comma + \ vector = Combine(lbracket + floatn('x') + comma +
floatn('y') + comma + floatn('z') + rbracket) floatn('y') + comma + floatn('z') + rbracket)
#direction definition # direction definition
simple_dir = oneOf(['X','Y','Z','XY','XZ','YZ']) simple_dir = oneOf(['X', 'Y', 'Z', 'XY', 'XZ', 'YZ'])
direction = simple_dir('simple_dir') | vector('vector_dir') direction = simple_dir('simple_dir') | vector('vector_dir')
#CQ type definition # CQ type definition
cqtype = oneOf(['Plane','Cylinder','Sphere','Cone','Line','Circle','Arc'], cqtype = oneOf(['Plane', 'Cylinder', 'Sphere', 'Cone', 'Line', 'Circle', 'Arc'],
caseless=True) caseless=True)
cqtype = cqtype.setParseAction(upcaseTokens) cqtype = cqtype.setParseAction(upcaseTokens)
#type operator # type operator
type_op = Literal('%') type_op = Literal('%')
#direction operator # direction operator
direction_op = oneOf(['>','<']) direction_op = oneOf(['>', '<'])
#index definition # index definition
ix_number = Group(Optional('-')+Word(nums)) ix_number = Group(Optional('-') + Word(nums))
lsqbracket = Literal('[').suppress() lsqbracket = Literal('[').suppress()
rsqbracket = Literal(']').suppress() rsqbracket = Literal(']').suppress()
index = lsqbracket + ix_number('index') + rsqbracket
#other operators
other_op = oneOf(['|','#','+','-'])
#named view
named_view = oneOf(['front','back','left','right','top','bottom'])
return direction('only_dir') | \
(type_op('type_op') + cqtype('cq_type')) | \
(direction_op('dir_op') + direction('dir') + Optional(index)) | \
(other_op('other_op') + direction('dir')) | \
named_view('named_view')
_grammar = _makeGrammar() #make a grammar instance index = lsqbracket + ix_number('index') + rsqbracket
# other operators
other_op = oneOf(['|', '#', '+', '-'])
# named view
named_view = oneOf(['front', 'back', 'left', 'right', 'top', 'bottom'])
return direction('only_dir') | \
(type_op('type_op') + cqtype('cq_type')) | \
(direction_op('dir_op') + direction('dir') + Optional(index)) | \
(other_op('other_op') + direction('dir')) | \
named_view('named_view')
_grammar = _makeGrammar() # make a grammar instance
class _SimpleStringSyntaxSelector(Selector): class _SimpleStringSyntaxSelector(Selector):
""" """
This is a private class that converts a parseResults object into a simple This is a private class that converts a parseResults object into a simple
selector object selector object
""" """
def __init__(self,parseResults):
def __init__(self, parseResults):
#define all token to object mappings
# define all token to object mappings
self.axes = { self.axes = {
'X': Vector(1,0,0), 'X': Vector(1, 0, 0),
'Y': Vector(0,1,0), 'Y': Vector(0, 1, 0),
'Z': Vector(0,0,1), 'Z': Vector(0, 0, 1),
'XY': Vector(1,1,0), 'XY': Vector(1, 1, 0),
'YZ': Vector(0,1,1), 'YZ': Vector(0, 1, 1),
'XZ': Vector(1,0,1) 'XZ': Vector(1, 0, 1)
} }
self.namedViews = { self.namedViews = {
'front' : (Vector(0,0,1),True), 'front': (Vector(0, 0, 1), True),
'back' : (Vector(0,0,1),False), 'back': (Vector(0, 0, 1), False),
'left' : (Vector(1,0,0),False), 'left': (Vector(1, 0, 0), False),
'right' : (Vector(1,0,0),True), 'right': (Vector(1, 0, 0), True),
'top' : (Vector(0,1,0),True), 'top': (Vector(0, 1, 0), True),
'bottom': (Vector(0,1,0),False) 'bottom': (Vector(0, 1, 0), False)
} }
self.operatorMinMax = { self.operatorMinMax = {
'>' : True, '>': True,
'<' : False, '<': False,
'+' : True, '+': True,
'-' : False '-': False
} }
self.operator = { self.operator = {
'+' : DirectionSelector, '+': DirectionSelector,
'-' : DirectionSelector, '-': DirectionSelector,
'#' : PerpendicularDirSelector, '#': PerpendicularDirSelector,
'|' : ParallelDirSelector} '|': ParallelDirSelector}
self.parseResults = parseResults self.parseResults = parseResults
self.mySelector = self._chooseSelector(parseResults) self.mySelector = self._chooseSelector(parseResults)
def _chooseSelector(self,pr): def _chooseSelector(self, pr):
""" """
Sets up the underlying filters accordingly Sets up the underlying filters accordingly
""" """
if 'only_dir' in pr: if 'only_dir' in pr:
vec = self._getVector(pr) vec = self._getVector(pr)
return DirectionSelector(vec) return DirectionSelector(vec)
elif 'type_op' in pr: elif 'type_op' in pr:
return TypeSelector(pr.cq_type) return TypeSelector(pr.cq_type)
elif 'dir_op' in pr: elif 'dir_op' in pr:
vec = self._getVector(pr) vec = self._getVector(pr)
minmax = self.operatorMinMax[pr.dir_op] minmax = self.operatorMinMax[pr.dir_op]
if 'index' in pr: if 'index' in pr:
return DirectionNthSelector(vec,int(''.join(pr.index.asList())),minmax) return DirectionNthSelector(vec, int(''.join(pr.index.asList())), minmax)
else: else:
return DirectionMinMaxSelector(vec,minmax) return DirectionMinMaxSelector(vec, minmax)
elif 'other_op' in pr: elif 'other_op' in pr:
vec = self._getVector(pr) vec = self._getVector(pr)
return self.operator[pr.other_op](vec) return self.operator[pr.other_op](vec)
else: else:
args = self.namedViews[pr.named_view] args = self.namedViews[pr.named_view]
return DirectionMinMaxSelector(*args) return DirectionMinMaxSelector(*args)
def _getVector(self,pr): def _getVector(self, pr):
""" """
Translate parsed vector string into a CQ Vector Translate parsed vector string into a CQ Vector
""" """
if 'vector_dir' in pr: if 'vector_dir' in pr:
vec = pr.vector_dir vec = pr.vector_dir
return Vector(float(vec.x),float(vec.y),float(vec.z)) return Vector(float(vec.x), float(vec.y), float(vec.z))
else: else:
return self.axes[pr.simple_dir] return self.axes[pr.simple_dir]
def filter(self,objectList): def filter(self, objectList):
""" """
selects minimum, maximum, positive or negative values relative to a direction selects minimum, maximum, positive or negative values relative to a direction
[+\|-\|<\|>\|] \<X\|Y\|Z> [+\|-\|<\|>\|] \<X\|Y\|Z>
""" """
return self.mySelector.filter(objectList) return self.mySelector.filter(objectList)
def _makeExpressionGrammar(atom): def _makeExpressionGrammar(atom):
""" """
Define the complex string selector grammar using PyParsing (which supports Define the complex string selector grammar using PyParsing (which supports
logical operations and nesting) logical operations and nesting)
""" """
#define operators # define operators
and_op = Literal('and') and_op = Literal('and')
or_op = Literal('or') or_op = Literal('or')
delta_op = oneOf(['exc','except']) delta_op = oneOf(['exc', 'except'])
not_op = Literal('not') not_op = Literal('not')
def atom_callback(res): def atom_callback(res):
return _SimpleStringSyntaxSelector(res) return _SimpleStringSyntaxSelector(res)
atom.setParseAction(atom_callback) #construct a simple selector from every matched # construct a simple selector from every matched
atom.setParseAction(atom_callback)
#define callback functions for all operations
# define callback functions for all operations
def and_callback(res): def and_callback(res):
items = res.asList()[0][::2] #take every secend items, i.e. all operands # take every secend items, i.e. all operands
return reduce(AndSelector,items) items = res.asList()[0][::2]
return reduce(AndSelector, items)
def or_callback(res): def or_callback(res):
items = res.asList()[0][::2] #take every secend items, i.e. all operands # take every secend items, i.e. all operands
return reduce(SumSelector,items) items = res.asList()[0][::2]
return reduce(SumSelector, items)
def exc_callback(res): def exc_callback(res):
items = res.asList()[0][::2] #take every secend items, i.e. all operands # take every secend items, i.e. all operands
return reduce(SubtractSelector,items) items = res.asList()[0][::2]
return reduce(SubtractSelector, items)
def not_callback(res): def not_callback(res):
right = res.asList()[0][1] #take second item, i.e. the operand right = res.asList()[0][1] # take second item, i.e. the operand
return InverseSelector(right) return InverseSelector(right)
#construct the final grammar and set all the callbacks # construct the final grammar and set all the callbacks
expr = infixNotation(atom, expr = infixNotation(atom,
[(and_op,2,opAssoc.LEFT,and_callback), [(and_op, 2, opAssoc.LEFT, and_callback),
(or_op,2,opAssoc.LEFT,or_callback), (or_op, 2, opAssoc.LEFT, or_callback),
(delta_op,2,opAssoc.LEFT,exc_callback), (delta_op, 2, opAssoc.LEFT, exc_callback),
(not_op,1,opAssoc.RIGHT,not_callback)]) (not_op, 1, opAssoc.RIGHT, not_callback)])
return expr return expr
_expression_grammar = _makeExpressionGrammar(_grammar) _expression_grammar = _makeExpressionGrammar(_grammar)
class StringSyntaxSelector(Selector): class StringSyntaxSelector(Selector):
""" """
Filter lists objects using a simple string syntax. All of the filters available in the string syntax Filter lists objects using a simple string syntax. All of the filters available in the string syntax
@ -627,10 +666,10 @@ class StringSyntaxSelector(Selector):
curve/surface type (same as :py:class:`TypeSelector`) curve/surface type (same as :py:class:`TypeSelector`)
***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ`` or ``(x,y,z)`` which defines an arbitrary direction ***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ`` or ``(x,y,z)`` which defines an arbitrary direction
It is possible to combine simple selectors together using logical operations. It is possible to combine simple selectors together using logical operations.
The following operations are suuported The following operations are suuported
:and: :and:
Logical AND, e.g. >X and >Y Logical AND, e.g. >X and >Y
:or: :or:
@ -642,22 +681,23 @@ class StringSyntaxSelector(Selector):
Finally, it is also possible to use even more complex expressions with nesting Finally, it is also possible to use even more complex expressions with nesting
and arbitrary number of terms, e.g. and arbitrary number of terms, e.g.
(not >X[0] and #XY) or >XY[0] (not >X[0] and #XY) or >XY[0]
Selectors are a complex topic: see :ref:`selector_reference` for more information Selectors are a complex topic: see :ref:`selector_reference` for more information
""" """
def __init__(self,selectorString):
def __init__(self, selectorString):
""" """
Feed the input string through the parser and construct an relevant complex selector object Feed the input string through the parser and construct an relevant complex selector object
""" """
self.selectorString = selectorString self.selectorString = selectorString
parse_result = _expression_grammar.parseString(selectorString, parse_result = _expression_grammar.parseString(selectorString,
parseAll=True) parseAll=True)
self.mySelector = parse_result.asList()[0] self.mySelector = parse_result.asList()[0]
def filter(self,objectList): def filter(self, objectList):
""" """
Filter give object list through th already constructed complex selector object Filter give object list through th already constructed complex selector object
""" """
return self.mySelector.filter(objectList) return self.mySelector.filter(objectList)

View File

@ -36,22 +36,24 @@ TEST_DEBUG_SCRIPT = textwrap.dedent(
""" """
) )
class TestCQGI(BaseTest): class TestCQGI(BaseTest):
def test_parser(self): def test_parser(self):
model = cqgi.CQModel(TESTSCRIPT) model = cqgi.CQModel(TESTSCRIPT)
metadata = model.metadata metadata = model.metadata
self.assertEquals(set(metadata.parameters.keys()), {'height', 'width', 'a', 'b', 'foo'}) self.assertEqual(set(metadata.parameters.keys()), {
'height', 'width', 'a', 'b', 'foo'})
def test_build_with_debug(self): def test_build_with_debug(self):
model = cqgi.CQModel(TEST_DEBUG_SCRIPT) model = cqgi.CQModel(TEST_DEBUG_SCRIPT)
result = model.build() result = model.build()
debugItems = result.debugObjects debugItems = result.debugObjects
self.assertTrue(len(debugItems) == 2) self.assertTrue(len(debugItems) == 2)
self.assertTrue( debugItems[0].object == "bar" ) self.assertTrue(debugItems[0].object == "bar")
self.assertTrue( debugItems[0].args == { "color":'yellow' } ) self.assertTrue(debugItems[0].args == {"color": 'yellow'})
self.assertTrue( debugItems[1].object == 2.0 ) self.assertTrue(debugItems[1].object == 2.0)
self.assertTrue( debugItems[1].args == {} ) self.assertTrue(debugItems[1].args == {})
def test_build_with_empty_params(self): def test_build_with_empty_params(self):
model = cqgi.CQModel(TESTSCRIPT) model = cqgi.CQModel(TESTSCRIPT)
@ -77,7 +79,7 @@ class TestCQGI(BaseTest):
a_param = model.metadata.parameters['a'] a_param = model.metadata.parameters['a']
self.assertTrue(a_param.default_value == 2.0) self.assertTrue(a_param.default_value == 2.0)
self.assertTrue(a_param.desc == 'FirstLetter') self.assertTrue(a_param.desc == 'FirstLetter')
self.assertTrue(a_param.varType == cqgi.NumberParameterType ) self.assertTrue(a_param.varType == cqgi.NumberParameterType)
def test_describe_parameter_invalid_doesnt_fail_script(self): def test_describe_parameter_invalid_doesnt_fail_script(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -88,8 +90,8 @@ class TestCQGI(BaseTest):
) )
model = cqgi.CQModel(script) model = cqgi.CQModel(script)
a_param = model.metadata.parameters['a'] a_param = model.metadata.parameters['a']
self.assertTrue(a_param.name == 'a' ) self.assertTrue(a_param.name == 'a')
def test_build_with_exception(self): def test_build_with_exception(self):
badscript = textwrap.dedent( badscript = textwrap.dedent(
""" """
@ -127,9 +129,9 @@ class TestCQGI(BaseTest):
model = cqgi.CQModel(script) model = cqgi.CQModel(script)
result = model.build({}) result = model.build({})
self.assertEquals(2, len(result.results)) self.assertEqual(2, len(result.results))
self.assertEquals(1, result.results[0]) self.assertEqual(1, result.results[0])
self.assertEquals(2, result.results[1]) self.assertEqual(2, result.results[1])
def test_that_assinging_number_to_string_works(self): def test_that_assinging_number_to_string_works(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -138,8 +140,8 @@ class TestCQGI(BaseTest):
build_object(h) build_object(h)
""" """
) )
result = cqgi.parse(script).build( {'h': 33.33}) result = cqgi.parse(script).build({'h': 33.33})
self.assertEquals(result.results[0], "33.33") self.assertEqual(result.results[0], "33.33")
def test_that_assigning_string_to_number_fails(self): def test_that_assigning_string_to_number_fails(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -148,8 +150,9 @@ class TestCQGI(BaseTest):
build_object(h) build_object(h)
""" """
) )
result = cqgi.parse(script).build( {'h': "a string"}) result = cqgi.parse(script).build({'h': "a string"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) self.assertTrue(isinstance(result.exception,
cqgi.InvalidParameterError))
def test_that_assigning_unknown_var_fails(self): def test_that_assigning_unknown_var_fails(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -159,8 +162,9 @@ class TestCQGI(BaseTest):
""" """
) )
result = cqgi.parse(script).build( {'w': "var is not there"}) result = cqgi.parse(script).build({'w': "var is not there"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) self.assertTrue(isinstance(result.exception,
cqgi.InvalidParameterError))
def test_that_not_calling_build_object_raises_error(self): def test_that_not_calling_build_object_raises_error(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -195,7 +199,7 @@ class TestCQGI(BaseTest):
result = cqgi.parse(script).build({'h': False}) result = cqgi.parse(script).build({'h': False})
self.assertTrue(result.success) self.assertTrue(result.success)
self.assertEquals(result.first_result,'*False*') self.assertEqual(result.first_result, '*False*')
def test_that_only_top_level_vars_are_detected(self): def test_that_only_top_level_vars_are_detected(self):
script = textwrap.dedent( script = textwrap.dedent(
@ -213,4 +217,4 @@ class TestCQGI(BaseTest):
model = cqgi.parse(script) model = cqgi.parse(script)
self.assertEquals(2, len(model.metadata.parameters)) self.assertEqual(2, len(model.metadata.parameters))

View File

@ -9,86 +9,92 @@ __author__ = 'dcowden'
""" """
import math import math
import unittest,sys import unittest
import sys
import os.path import os.path
#my modules # my modules
from tests import BaseTest,makeUnitCube,makeUnitSquareWire from tests import BaseTest, makeUnitCube, makeUnitSquareWire
from cadquery import * from cadquery import *
from cadquery import selectors from cadquery import selectors
class TestCQSelectors(BaseTest):
class TestCQSelectors(BaseTest):
def testWorkplaneCenter(self): def testWorkplaneCenter(self):
"Test Moving workplane center" "Test Moving workplane center"
s = Workplane(Plane.XY()) s = Workplane(Plane.XY())
#current point and world point should be equal # current point and world point should be equal
self.assertTupleAlmostEquals((0.0,0.0,0.0),s.plane.origin.toTuple(),3) self.assertTupleAlmostEquals(
(0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3)
#move origin and confirm center moves # move origin and confirm center moves
s.center(-2.0,-2.0) s.center(-2.0, -2.0)
#current point should be 0,0, but # current point should be 0,0, but
self.assertTupleAlmostEquals((-2.0,-2.0,0.0),s.plane.origin.toTuple(),3)
self.assertTupleAlmostEquals(
(-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3)
def testVertices(self): def testVertices(self):
t = makeUnitSquareWire() # square box t = makeUnitSquareWire() # square box
c = CQ(t) c = CQ(t)
self.assertEqual(4,c.vertices().size() ) self.assertEqual(4, c.vertices().size())
self.assertEqual(4,c.edges().size() ) self.assertEqual(4, c.edges().size())
self.assertEqual(0,c.vertices().edges().size() ) #no edges on any vertices self.assertEqual(0, c.vertices().edges().size()
self.assertEqual(4,c.edges().vertices().size() ) #but selecting all edges still yields all vertices ) # no edges on any vertices
self.assertEqual(1,c.wires().size()) #just one wire # but selecting all edges still yields all vertices
self.assertEqual(0,c.faces().size()) self.assertEqual(4, c.edges().vertices().size())
self.assertEqual(0,c.vertices().faces().size()) #odd combinations all work but yield no results self.assertEqual(1, c.wires().size()) # just one wire
self.assertEqual(0,c.edges().faces().size()) self.assertEqual(0, c.faces().size())
self.assertEqual(0,c.edges().vertices().faces().size()) # odd combinations all work but yield no results
self.assertEqual(0, c.vertices().faces().size())
self.assertEqual(0, c.edges().faces().size())
self.assertEqual(0, c.edges().vertices().faces().size())
def testEnd(self): def testEnd(self):
c = CQ(makeUnitSquareWire()) c = CQ(makeUnitSquareWire())
self.assertEqual(4,c.vertices().size() ) #4 because there are 4 vertices # 4 because there are 4 vertices
self.assertEqual(1,c.vertices().end().size() ) #1 because we started with 1 wire self.assertEqual(4, c.vertices().size())
# 1 because we started with 1 wire
self.assertEqual(1, c.vertices().end().size())
def testAll(self): def testAll(self):
"all returns a list of CQ objects, so that you can iterate over them individually" "all returns a list of CQ objects, so that you can iterate over them individually"
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
self.assertEqual(6,c.faces().size()) self.assertEqual(6, c.faces().size())
self.assertEqual(6,len(c.faces().all())) self.assertEqual(6, len(c.faces().all()))
self.assertEqual(4,c.faces().all()[0].vertices().size() ) self.assertEqual(4, c.faces().all()[0].vertices().size())
def testFirst(self): def testFirst(self):
c = CQ( makeUnitCube()) c = CQ(makeUnitCube())
self.assertEqual(type(c.vertices().first().val()),Vertex) self.assertEqual(type(c.vertices().first().val()), Vertex)
self.assertEqual(type(c.vertices().first().first().first().val()),Vertex) self.assertEqual(
type(c.vertices().first().first().first().val()), Vertex)
def testCompounds(self): def testCompounds(self):
c = CQ(makeUnitSquareWire()) c = CQ(makeUnitSquareWire())
self.assertEqual(0,c.compounds().size() ) self.assertEqual(0, c.compounds().size())
self.assertEqual(0,c.shells().size() ) self.assertEqual(0, c.shells().size())
self.assertEqual(0,c.solids().size() ) self.assertEqual(0, c.solids().size())
def testSolid(self): def testSolid(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
#make sure all the counts are right for a cube # make sure all the counts are right for a cube
self.assertEqual(1,c.solids().size() ) self.assertEqual(1, c.solids().size())
self.assertEqual(6,c.faces().size() ) self.assertEqual(6, c.faces().size())
self.assertEqual(12,c.edges().size()) self.assertEqual(12, c.edges().size())
self.assertEqual(8,c.vertices().size() ) self.assertEqual(8, c.vertices().size())
self.assertEqual(0,c.compounds().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() )
# 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): def testFaceTypesFilter(self):
"Filters by face type" "Filters by face type"
@ -102,16 +108,16 @@ class TestCQSelectors(BaseTest):
def testPerpendicularDirFilter(self): def testPerpendicularDirFilter(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
self.assertEqual(8,c.edges("#Z").size() ) #8 edges are perp. to z 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! self.assertEqual(4, c.faces("#Z").size()) # 4 faces are perp to z too!
def testFaceDirFilter(self): def testFaceDirFilter(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
#a cube has one face in each direction # 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("-Z").size()) self.assertEqual(1, c.faces("-Z").size())
self.assertEqual(1, c.faces("+X").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()) # should be same as +X
self.assertEqual(1, c.faces("-X").size()) self.assertEqual(1, c.faces("-X").size())
self.assertEqual(1, c.faces("+Y").size()) self.assertEqual(1, c.faces("+Y").size())
self.assertEqual(1, c.faces("-Y").size()) self.assertEqual(1, c.faces("-Y").size())
@ -120,13 +126,15 @@ class TestCQSelectors(BaseTest):
def testParallelPlaneFaceFilter(self): def testParallelPlaneFaceFilter(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
#faces parallel to Z axis # faces parallel to Z axis
self.assertEqual(2, c.faces("|Z").size()) self.assertEqual(2, c.faces("|Z").size())
#TODO: provide short names for ParallelDirSelector # TODO: provide short names for ParallelDirSelector
self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,1)))).size()) #same thing as above self.assertEqual(2, c.faces(selectors.ParallelDirSelector(
self.assertEqual(2, c.faces(selectors.ParallelDirSelector(Vector((0,0,-1)))).size()) #same thing as above Vector((0, 0, 1)))).size()) # same thing as above
self.assertEqual(2, c.faces(selectors.ParallelDirSelector(
Vector((0, 0, -1)))).size()) # same thing as above
#just for fun, vertices on faces parallel to z # just for fun, vertices on faces parallel to z
self.assertEqual(8, c.faces("|Z").vertices().size()) self.assertEqual(8, c.faces("|Z").vertices().size())
def testParallelEdgeFilter(self): def testParallelEdgeFilter(self):
@ -138,14 +146,14 @@ class TestCQSelectors(BaseTest):
def testMaxDistance(self): def testMaxDistance(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
#should select the topmost face # should select the topmost face
self.assertEqual(1, c.faces(">Z").size()) self.assertEqual(1, c.faces(">Z").size())
self.assertEqual(4, c.faces(">Z").vertices().size()) self.assertEqual(4, c.faces(">Z").vertices().size())
#vertices should all be at z=1, if this is the top face # vertices should all be at z=1, if this is the top face
self.assertEqual(4, len(c.faces(">Z").vertices().vals() )) self.assertEqual(4, len(c.faces(">Z").vertices().vals()))
for v in c.faces(">Z").vertices().vals(): for v in c.faces(">Z").vertices().vals():
self.assertAlmostEqual(1.0,v.Z,3) self.assertAlmostEqual(1.0, v.Z, 3)
# test the case of multiple objects at the same distance # test the case of multiple objects at the same distance
el = c.edges("<Z").vals() el = c.edges("<Z").vals()
@ -154,125 +162,130 @@ class TestCQSelectors(BaseTest):
def testMinDistance(self): def testMinDistance(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
#should select the topmost face # should select the topmost face
self.assertEqual(1, c.faces("<Z").size()) self.assertEqual(1, c.faces("<Z").size())
self.assertEqual(4, c.faces("<Z").vertices().size()) self.assertEqual(4, c.faces("<Z").vertices().size())
#vertices should all be at z=1, if this is the top face # vertices should all be at z=1, if this is the top face
self.assertEqual(4, len(c.faces("<Z").vertices().vals() )) self.assertEqual(4, len(c.faces("<Z").vertices().vals()))
for v in c.faces("<Z").vertices().vals(): for v in c.faces("<Z").vertices().vals():
self.assertAlmostEqual(0.0,v.Z,3) self.assertAlmostEqual(0.0, v.Z, 3)
# test the case of multiple objects at the same distance # test the case of multiple objects at the same distance
el = c.edges("<Z").vals() el = c.edges("<Z").vals()
self.assertEqual(4, len(el)) self.assertEqual(4, len(el))
def testNthDistance(self): def testNthDistance(self):
c = Workplane('XY').pushPoints([(-2,0),(2,0)]).box(1,1,1) c = Workplane('XY').pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1)
#2nd face # 2nd face
val = c.faces(selectors.DirectionNthSelector(Vector(1,0,0),1)).val() val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val()
self.assertAlmostEqual(val.Center().x,-1.5) self.assertAlmostEqual(val.Center().x, -1.5)
#2nd face with inversed selection vector # 2nd face with inversed selection vector
val = c.faces(selectors.DirectionNthSelector(Vector(-1,0,0),1)).val() val = c.faces(selectors.DirectionNthSelector(
self.assertAlmostEqual(val.Center().x,1.5) Vector(-1, 0, 0), 1)).val()
self.assertAlmostEqual(val.Center().x, 1.5)
#2nd last face
val = c.faces(selectors.DirectionNthSelector(Vector(1,0,0),-2)).val() # 2nd last face
self.assertAlmostEqual(val.Center().x,1.5) val = c.faces(selectors.DirectionNthSelector(
Vector(1, 0, 0), -2)).val()
#Last face self.assertAlmostEqual(val.Center().x, 1.5)
val = c.faces(selectors.DirectionNthSelector(Vector(1,0,0),-1)).val()
self.assertAlmostEqual(val.Center().x,2.5) # Last face
val = c.faces(selectors.DirectionNthSelector(
#check if the selected face if normal to the specified Vector Vector(1, 0, 0), -1)).val()
self.assertAlmostEqual(val.normalAt().cross(Vector(1,0,0)).Length,0.0) self.assertAlmostEqual(val.Center().x, 2.5)
#repeat the test using string based selector # check if the selected face if normal to the specified Vector
self.assertAlmostEqual(
#2nd face val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
# repeat the test using string based selector
# 2nd face
val = c.faces('>(1,0,0)[1]').val() val = c.faces('>(1,0,0)[1]').val()
self.assertAlmostEqual(val.Center().x,-1.5) self.assertAlmostEqual(val.Center().x, -1.5)
val = c.faces('>X[1]').val() val = c.faces('>X[1]').val()
self.assertAlmostEqual(val.Center().x,-1.5) self.assertAlmostEqual(val.Center().x, -1.5)
#2nd face with inversed selection vector # 2nd face with inversed selection vector
val = c.faces('>(-1,0,0)[1]').val() val = c.faces('>(-1,0,0)[1]').val()
self.assertAlmostEqual(val.Center().x,1.5) self.assertAlmostEqual(val.Center().x, 1.5)
val = c.faces('<X[1]').val() val = c.faces('<X[1]').val()
self.assertAlmostEqual(val.Center().x,1.5) self.assertAlmostEqual(val.Center().x, 1.5)
#2nd last face # 2nd last face
val = c.faces('>X[-2]').val() val = c.faces('>X[-2]').val()
self.assertAlmostEqual(val.Center().x,1.5) self.assertAlmostEqual(val.Center().x, 1.5)
#Last face # Last face
val = c.faces('>X[-1]').val() val = c.faces('>X[-1]').val()
self.assertAlmostEqual(val.Center().x,2.5) self.assertAlmostEqual(val.Center().x, 2.5)
#check if the selected face if normal to the specified Vector # check if the selected face if normal to the specified Vector
self.assertAlmostEqual(val.normalAt().cross(Vector(1,0,0)).Length,0.0) self.assertAlmostEqual(
val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
#test selection of multiple faces with the same distance
# test selection of multiple faces with the same distance
c = Workplane('XY')\ c = Workplane('XY')\
.box(1,4,1,centered=(False,True,False)).faces('<Z')\ .box(1, 4, 1, centered=(False, True, False)).faces('<Z')\
.box(2,2,2,centered=(True,True,False)).faces('>Z')\ .box(2, 2, 2, centered=(True, True, False)).faces('>Z')\
.box(1,1,1,centered=(True,True,False)) .box(1, 1, 1, centered=(True, True, False))
#select 2nd from the bottom (NB python indexing is 0-based) # select 2nd from the bottom (NB python indexing is 0-based)
vals = c.faces('>Z[1]').vals() vals = c.faces('>Z[1]').vals()
self.assertEqual(len(vals),2) self.assertEqual(len(vals), 2)
val = c.faces('>Z[1]').val() val = c.faces('>Z[1]').val()
self.assertAlmostEqual(val.Center().z,1) self.assertAlmostEqual(val.Center().z, 1)
#do the same but by selecting 3rd from the top # do the same but by selecting 3rd from the top
vals = c.faces('<Z[2]').vals() vals = c.faces('<Z[2]').vals()
self.assertEqual(len(vals),2) self.assertEqual(len(vals), 2)
val = c.faces('<Z[2]').val() val = c.faces('<Z[2]').val()
self.assertAlmostEqual(val.Center().z,1) self.assertAlmostEqual(val.Center().z, 1)
#do the same but by selecting 2nd last from the bottom # do the same but by selecting 2nd last from the bottom
vals = c.faces('<Z[-2]').vals() vals = c.faces('<Z[-2]').vals()
self.assertEqual(len(vals),2) self.assertEqual(len(vals), 2)
val = c.faces('<Z[-2]').val() val = c.faces('<Z[-2]').val()
self.assertAlmostEqual(val.Center().z,1) self.assertAlmostEqual(val.Center().z, 1)
#verify that <Z[-1] is equivalent to <Z # verify that <Z[-1] is equivalent to <Z
val1 = c.faces('<Z[-1]').val() val1 = c.faces('<Z[-1]').val()
val2 = c.faces('<Z').val() val2 = c.faces('<Z').val()
self.assertTupleAlmostEquals(val1.Center().toTuple(), self.assertTupleAlmostEquals(val1.Center().toTuple(),
val2.Center().toTuple(), val2.Center().toTuple(),
3) 3)
#verify that >Z[-1] is equivalent to >Z # verify that >Z[-1] is equivalent to >Z
val1 = c.faces('>Z[-1]').val() val1 = c.faces('>Z[-1]').val()
val2 = c.faces('>Z').val() val2 = c.faces('>Z').val()
self.assertTupleAlmostEquals(val1.Center().toTuple(), self.assertTupleAlmostEquals(val1.Center().toTuple(),
val2.Center().toTuple(), val2.Center().toTuple(),
3) 3)
def testNearestTo(self): def testNearestTo(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
#nearest vertex to origin is (0,0,0) # nearest vertex to origin is (0,0,0)
t = (0.1,0.1,0.1) t = (0.1, 0.1, 0.1)
v = c.vertices(selectors.NearestToPointSelector(t)).vals()[0] v = c.vertices(selectors.NearestToPointSelector(t)).vals()[0]
self.assertTupleAlmostEquals((0.0,0.0,0.0),(v.X,v.Y,v.Z),3) self.assertTupleAlmostEquals((0.0, 0.0, 0.0), (v.X, v.Y, v.Z), 3)
t = (0.1,0.1,0.2) t = (0.1, 0.1, 0.2)
#nearest edge is the vertical side edge, 0,0,0 -> 0,0,1 # nearest edge is the vertical side edge, 0,0,0 -> 0,0,1
e = c.edges(selectors.NearestToPointSelector(t)).vals()[0] e = c.edges(selectors.NearestToPointSelector(t)).vals()[0]
v = c.edges(selectors.NearestToPointSelector(t)).vertices().vals() v = c.edges(selectors.NearestToPointSelector(t)).vertices().vals()
self.assertEqual(2,len(v)) self.assertEqual(2, len(v))
#nearest solid is myself # nearest solid is myself
s = c.solids(selectors.NearestToPointSelector(t)).vals() s = c.solids(selectors.NearestToPointSelector(t)).vals()
self.assertEqual(1,len(s)) self.assertEqual(1, len(s))
def testBox(self): def testBox(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
@ -303,9 +316,11 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3) self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
# test multiple vertices selection # test multiple vertices selection
vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, 0.9),(0.1, 1.1, 1.1))).vals() vl = c.vertices(selectors.BoxSelector(
(-0.1, -0.1, 0.9), (0.1, 1.1, 1.1))).vals()
self.assertEqual(2, len(vl)) self.assertEqual(2, len(vl))
vl = c.vertices(selectors.BoxSelector((-0.1, -0.1, -0.1),(0.1, 1.1, 1.1))).vals() vl = c.vertices(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (0.1, 1.1, 1.1))).vals()
self.assertEqual(4, len(vl)) self.assertEqual(4, len(vl))
# test edge selection # test edge selection
@ -330,9 +345,11 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3) self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
# test multiple edge selection # test multiple edge selection
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals() el = c.edges(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals()
self.assertEqual(2, len(el)) self.assertEqual(2, len(el))
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals() el = c.edges(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals()
self.assertEqual(3, len(el)) self.assertEqual(3, len(el))
# test face selection # test face selection
@ -357,17 +374,22 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3) self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
# test multiple face selection # test multiple face selection
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals() fl = c.faces(selectors.BoxSelector(
(0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals()
self.assertEqual(2, len(fl)) self.assertEqual(2, len(fl))
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals() fl = c.faces(selectors.BoxSelector(
(0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals()
self.assertEqual(3, len(fl)) self.assertEqual(3, len(fl))
# test boundingbox option # test boundingbox option
el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals() el = c.edges(selectors.BoxSelector(
(-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True)).vals()
self.assertEqual(1, len(el)) self.assertEqual(1, len(el))
fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals() fl = c.faces(selectors.BoxSelector(
(0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True)).vals()
self.assertEqual(0, len(fl)) self.assertEqual(0, len(fl))
fl = c.faces(selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals() fl = c.faces(selectors.BoxSelector(
(-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True)).vals()
self.assertEqual(1, len(fl)) self.assertEqual(1, len(fl))
def testAndSelector(self): def testAndSelector(self):
@ -376,13 +398,14 @@ class TestCQSelectors(BaseTest):
S = selectors.StringSyntaxSelector S = selectors.StringSyntaxSelector
BS = selectors.BoxSelector BS = selectors.BoxSelector
el = c.edges(selectors.AndSelector(S('|X'), BS((-2,-2,0.1), (2,2,2)))).vals() el = c.edges(selectors.AndSelector(
S('|X'), BS((-2, -2, 0.1), (2, 2, 2)))).vals()
self.assertEqual(2, len(el)) self.assertEqual(2, len(el))
# test 'and' (intersection) operator # test 'and' (intersection) operator
el = c.edges(S('|X') & BS((-2,-2,0.1), (2,2,2))).vals() el = c.edges(S('|X') & BS((-2, -2, 0.1), (2, 2, 2))).vals()
self.assertEqual(2, len(el)) self.assertEqual(2, len(el))
# test using extended string syntax # test using extended string syntax
v = c.vertices(">X and >Y").vals() v = c.vertices(">X and >Y").vals()
self.assertEqual(2, len(v)) self.assertEqual(2, len(v))
@ -402,7 +425,7 @@ class TestCQSelectors(BaseTest):
self.assertEqual(2, len(fl)) self.assertEqual(2, len(fl))
el = c.edges(S("|X") + S("|Y")).vals() el = c.edges(S("|X") + S("|Y")).vals()
self.assertEqual(8, len(el)) self.assertEqual(8, len(el))
# test using extended string syntax # test using extended string syntax
fl = c.faces(">Z or <Z").vals() fl = c.faces(">Z or <Z").vals()
self.assertEqual(2, len(fl)) self.assertEqual(2, len(fl))
@ -420,7 +443,7 @@ class TestCQSelectors(BaseTest):
# test the subtract operator # test the subtract operator
fl = c.faces(S("#Z") - S(">X")).vals() fl = c.faces(S("#Z") - S(">X")).vals()
self.assertEqual(3, len(fl)) self.assertEqual(3, len(fl))
# test using extended string syntax # test using extended string syntax
fl = c.faces("#Z exc >X").vals() fl = c.faces("#Z exc >X").vals()
self.assertEqual(3, len(fl)) self.assertEqual(3, len(fl))
@ -440,43 +463,42 @@ class TestCQSelectors(BaseTest):
self.assertEqual(5, len(fl)) self.assertEqual(5, len(fl))
el = c.faces('>Z').edges(-S('>X')).vals() el = c.faces('>Z').edges(-S('>X')).vals()
self.assertEqual(3, len(el)) self.assertEqual(3, len(el))
# test using extended string syntax # test using extended string syntax
fl = c.faces('not >Z').vals() fl = c.faces('not >Z').vals()
self.assertEqual(5, len(fl)) self.assertEqual(5, len(fl))
el = c.faces('>Z').edges('not >X').vals() el = c.faces('>Z').edges('not >X').vals()
self.assertEqual(3, len(el)) self.assertEqual(3, len(el))
def testComplexStringSelector(self): def testComplexStringSelector(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
v = c.vertices('(>X and >Y) or (<X and <Y)').vals() v = c.vertices('(>X and >Y) or (<X and <Y)').vals()
self.assertEqual(4, len(v)) self.assertEqual(4, len(v))
def testFaceCount(self): def testFaceCount(self):
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
self.assertEqual( 6, c.faces().size() ) self.assertEqual(6, c.faces().size())
self.assertEqual( 2, c.faces("|Z").size() ) self.assertEqual(2, c.faces("|Z").size())
def testVertexFilter(self): def testVertexFilter(self):
"test selecting vertices on a face" "test selecting vertices on a face"
c = CQ(makeUnitCube()) c = CQ(makeUnitCube())
#TODO: filters work ok, but they are in global coordinates which sux. it would be nice # 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 # if they were available in coordinates local to the selected face
v2 = c.faces("+Z").vertices("<XY") v2 = c.faces("+Z").vertices("<XY")
self.assertEqual(1,v2.size() ) #another way self.assertEqual(1, v2.size()) # another way
#make sure the vertex is the right one # make sure the vertex is the right one
self.assertTupleAlmostEquals((0.0, 0.0, 1.0), v2.val().toTuple(), 3)
self.assertTupleAlmostEquals((0.0,0.0,1.0),v2.val().toTuple() ,3)
def testGrammar(self): def testGrammar(self):
""" """
Test if reasonable string selector expressions parse without an error Test if reasonable string selector expressions parse without an error
""" """
gram = selectors._expression_grammar gram = selectors._expression_grammar
expressions = ['+X ', expressions = ['+X ',
@ -499,5 +521,5 @@ class TestCQSelectors(BaseTest):
'(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)', '(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)',
'not ( <X or >X or <Y or >Y )'] 'not ( <X or >X or <Y or >Y )']
for e in expressions: gram.parseString(e,parseAll=True) for e in expressions:
gram.parseString(e, parseAll=True)

View File

@ -1,4 +1,4 @@
#system modules # system modules
import sys import sys
import unittest import unittest
from tests import BaseTest from tests import BaseTest
@ -6,16 +6,17 @@ from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_DZ
from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex, from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakeFace) BRepBuilderAPI_MakeFace)
from OCC.GC import GC_MakeCircle from OCC.GC import GC_MakeCircle
from cadquery import * from cadquery import *
class TestCadObjects(BaseTest): class TestCadObjects(BaseTest):
def _make_circle(self): def _make_circle(self):
circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3),gp_DZ()), circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp_DZ()),
2.) 2.)
return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge()) return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge())
@ -33,34 +34,39 @@ class TestCadObjects(BaseTest):
""" """
v = Vertex.makeVertex(1, 1, 1) v = Vertex.makeVertex(1, 1, 1)
self.assertEqual(1, v.X) self.assertEqual(1, v.X)
self.assertEquals(Vector, type(v.Center())) self.assertEqual(Vector, type(v.Center()))
def testBasicBoundingBox(self): def testBasicBoundingBox(self):
v = Vertex.makeVertex(1, 1, 1) v = Vertex.makeVertex(1, 1, 1)
v2 = Vertex.makeVertex(2, 2, 2) v2 = Vertex.makeVertex(2, 2, 2)
self.assertEquals(BoundBox, type(v.BoundingBox())) self.assertEqual(BoundBox, type(v.BoundingBox()))
self.assertEquals(BoundBox, type(v2.BoundingBox())) self.assertEqual(BoundBox, type(v2.BoundingBox()))
bb1 = v.BoundingBox().add(v2.BoundingBox()) bb1 = v.BoundingBox().add(v2.BoundingBox())
self.assertAlmostEquals(bb1.xlen, 1.0, 1) #OCC uses some approximations # OCC uses some approximations
self.assertAlmostEqual(bb1.xlen, 1.0, 1)
def testEdgeWrapperCenter(self): def testEdgeWrapperCenter(self):
e = self._make_circle() e = self._make_circle()
self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3) self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3)
def testEdgeWrapperMakeCircle(self): def testEdgeWrapperMakeCircle(self):
halfCircleEdge = Edge.makeCircle(radius=10, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180) halfCircleEdge = Edge.makeCircle(radius=10, pnt=(
0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180)
#self.assertTupleAlmostEquals((0.0, 5.0, 0.0), halfCircleEdge.CenterOfBoundBox(0.0001).toTuple(),3) #self.assertTupleAlmostEquals((0.0, 5.0, 0.0), halfCircleEdge.CenterOfBoundBox(0.0001).toTuple(),3)
self.assertTupleAlmostEquals((10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3) self.assertTupleAlmostEquals(
self.assertTupleAlmostEquals((-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3) (10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3)
self.assertTupleAlmostEquals(
(-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3)
def testFaceWrapperMakePlane(self): def testFaceWrapperMakePlane(self):
mplane = Face.makePlane(10,10) mplane = Face.makePlane(10, 10)
self.assertTupleAlmostEquals((0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3) self.assertTupleAlmostEquals(
(0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3)
def testCenterOfBoundBox(self): def testCenterOfBoundBox(self):
pass pass
@ -72,6 +78,7 @@ class TestCadObjects(BaseTest):
""" """
Tests whether or not a proper weighted center can be found for a compound Tests whether or not a proper weighted center can be found for a compound
""" """
def cylinders(self, radius, height): def cylinders(self, radius, height):
def _cyl(pnt): def _cyl(pnt):
# Inner function to build a cylinder # Inner function to build a cylinder
@ -85,22 +92,24 @@ class TestCadObjects(BaseTest):
Workplane.cyl = cylinders Workplane.cyl = cylinders
# Now test. here we want weird workplane to see if the objects are transformed right # Now test. here we want weird workplane to see if the objects are transformed right
s = Workplane("XY").rect(2.0, 3.0, forConstruction=True).vertices().cyl(0.25, 0.5) s = Workplane("XY").rect(
2.0, 3.0, forConstruction=True).vertices().cyl(0.25, 0.5)
self.assertEquals(4, len(s.val().Solids())) self.assertEqual(4, len(s.val().Solids()))
self.assertTupleAlmostEquals((0.0, 0.0, 0.25), s.val().Center().toTuple(), 3) self.assertTupleAlmostEquals(
(0.0, 0.0, 0.25), s.val().Center().toTuple(), 3)
def testDot(self): def testDot(self):
v1 = Vector(2, 2, 2) v1 = Vector(2, 2, 2)
v2 = Vector(1, -1, 1) v2 = Vector(1, -1, 1)
self.assertEquals(2.0, v1.dot(v2)) self.assertEqual(2.0, v1.dot(v2))
def testVectorAdd(self): def testVectorAdd(self):
result = Vector(1, 2, 0) + Vector(0, 0, 3) result = Vector(1, 2, 0) + Vector(0, 0, 3)
self.assertTupleAlmostEquals((1.0, 2.0, 3.0), result.toTuple(), 3) self.assertTupleAlmostEquals((1.0, 2.0, 3.0), result.toTuple(), 3)
def testTranslate(self): def testTranslate(self):
e = Edge.makeCircle(2,(1,2,3)) e = Edge.makeCircle(2, (1, 2, 3))
e2 = e.translate(Vector(0, 0, 1)) e2 = e.translate(Vector(0, 0, 1))
self.assertTupleAlmostEquals((1.0, 2.0, 4.0), e2.Center().toTuple(), 3) self.assertTupleAlmostEquals((1.0, 2.0, 4.0), e2.Center().toTuple(), 3)
@ -108,7 +117,8 @@ class TestCadObjects(BaseTest):
def testVertices(self): def testVertices(self):
e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0),
gp_Pnt(1, 1, 0)).Edge()) gp_Pnt(1, 1, 0)).Edge())
self.assertEquals(2, len(e.Vertices())) self.assertEqual(2, len(e.Vertices()))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,49 @@
""" """
Tests basic workplane functionality Tests basic workplane functionality
""" """
#core modules # core modules
import StringIO import sys
if sys.version_info.major == 2:
import cStringIO as StringIO
else:
import io as StringIO
#my modules # my modules
from cadquery import * from cadquery import *
from cadquery import exporters from cadquery import exporters
from tests import BaseTest from tests import BaseTest
class TestExporters(BaseTest): class TestExporters(BaseTest):
def _exportBox(self,eType,stringsToFind): def _exportBox(self, eType, stringsToFind):
""" """
Exports a test object, and then looks for Exports a test object, and then looks for
all of the supplied strings to be in the result all of the supplied strings to be in the result
returns the result in case the case wants to do more checks also returns the result in case the case wants to do more checks also
""" """
p = Workplane("XY").box(1,2,3) p = Workplane("XY").box(1, 2, 3)
s = StringIO.StringIO() s = StringIO.StringIO()
exporters.exportShape(p,eType,s,0.1) exporters.exportShape(p, eType, s, 0.1)
result = s.getvalue() result = s.getvalue()
for q in stringsToFind: for q in stringsToFind:
self.assertTrue(result.find(q) > -1 ) self.assertTrue(result.find(q) > -1)
return result return result
def testSTL(self): def testSTL(self):
self._exportBox(exporters.ExportTypes.STL,['facet normal']) self._exportBox(exporters.ExportTypes.STL, ['facet normal'])
def testSVG(self): def testSVG(self):
self._exportBox(exporters.ExportTypes.SVG,['<svg','<g transform']) self._exportBox(exporters.ExportTypes.SVG, ['<svg', '<g transform'])
def testAMF(self): def testAMF(self):
self._exportBox(exporters.ExportTypes.AMF,['<amf units','</object>']) self._exportBox(exporters.ExportTypes.AMF, ['<amf units', '</object>'])
def testSTEP(self): def testSTEP(self):
self._exportBox(exporters.ExportTypes.STEP,['FILE_SCHEMA']) self._exportBox(exporters.ExportTypes.STEP, ['FILE_SCHEMA'])
def testTJS(self): def testTJS(self):
self._exportBox(exporters.ExportTypes.TJS,['vertices','formatVersion','faces']) self._exportBox(exporters.ExportTypes.TJS, [
'vertices', 'formatVersion', 'faces'])

View File

@ -1,15 +1,15 @@
""" """
Tests file importers such as STEP Tests file importers such as STEP
""" """
#core modules # core modules
import StringIO import io
from cadquery import * from cadquery import *
from cadquery import exporters from cadquery import exporters
from cadquery import importers from cadquery import importers
from tests import BaseTest from tests import BaseTest
#where unit test output will be saved # where unit test output will be saved
import sys import sys
if sys.platform.startswith("win"): if sys.platform.startswith("win"):
OUTDIR = "c:/temp" OUTDIR = "c:/temp"
@ -24,24 +24,27 @@ class TestImporters(BaseTest):
:param importType: The type of file we're importing (STEP, STL, etc) :param importType: The type of file we're importing (STEP, STL, etc)
:param fileName: The path and name of the file to write to :param fileName: The path and name of the file to write to
""" """
#We're importing a STEP file # We're importing a STEP file
if importType == importers.ImportTypes.STEP: if importType == importers.ImportTypes.STEP:
#We first need to build a simple shape to export # We first need to build a simple shape to export
shape = Workplane("XY").box(1, 2, 3).val() shape = Workplane("XY").box(1, 2, 3).val()
#Export the shape to a temporary file # Export the shape to a temporary file
shape.exportStep(fileName) shape.exportStep(fileName)
# Reimport the shape from the new STEP file # Reimport the shape from the new STEP file
importedShape = importers.importShape(importType,fileName) importedShape = importers.importShape(importType, fileName)
#Check to make sure we got a solid back # Check to make sure we got a solid back
self.assertTrue(importedShape.val().ShapeType() == "Solid") self.assertTrue(importedShape.val().ShapeType() == "Solid")
#Check the number of faces and vertices per face to make sure we have a box shape # Check the number of faces and vertices per face to make sure we have a box shape
self.assertTrue(importedShape.faces("+X").size() == 1 and importedShape.faces("+X").vertices().size() == 4) self.assertTrue(importedShape.faces("+X").size() ==
self.assertTrue(importedShape.faces("+Y").size() == 1 and importedShape.faces("+Y").vertices().size() == 4) 1 and importedShape.faces("+X").vertices().size() == 4)
self.assertTrue(importedShape.faces("+Z").size() == 1 and importedShape.faces("+Z").vertices().size() == 4) self.assertTrue(importedShape.faces("+Y").size() ==
1 and importedShape.faces("+Y").vertices().size() == 4)
self.assertTrue(importedShape.faces("+Z").size() ==
1 and importedShape.faces("+Z").vertices().size() == 4)
def testSTEP(self): def testSTEP(self):
""" """
@ -49,6 +52,7 @@ class TestImporters(BaseTest):
""" """
self.importBox(importers.ImportTypes.STEP, OUTDIR + "/tempSTEP.step") self.importBox(importers.ImportTypes.STEP, OUTDIR + "/tempSTEP.step")
if __name__ == '__main__': if __name__ == '__main__':
import unittest import unittest
unittest.main() unittest.main()

View File

@ -1,11 +1,11 @@
""" """
Tests basic workplane functionality Tests basic workplane functionality
""" """
#core modules # core modules
#my modules # my modules
from cadquery import * from cadquery import *
from tests import BaseTest,toTuple from tests import BaseTest, toTuple
xAxis_ = Vector(1, 0, 0) xAxis_ = Vector(1, 0, 0)
yAxis_ = Vector(0, 1, 0) yAxis_ = Vector(0, 1, 0)
@ -14,79 +14,97 @@ xInvAxis_ = Vector(-1, 0, 0)
yInvAxis_ = Vector(0, -1, 0) yInvAxis_ = Vector(0, -1, 0)
zInvAxis_ = Vector(0, 0, -1) zInvAxis_ = Vector(0, 0, -1)
class TestWorkplanes(BaseTest): class TestWorkplanes(BaseTest):
def testYZPlaneOrigins(self): def testYZPlaneOrigins(self):
#xy plane-- with origin at x=0.25 # xy plane-- with origin at x=0.25
base = Vector(0.25,0,0) base = Vector(0.25, 0, 0)
p = Plane(base, Vector(0,1,0), Vector(1,0,0)) p = Plane(base, Vector(0, 1, 0), Vector(1, 0, 0))
#origin is always (0,0,0) in local coordinates # origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 ) self.assertTupleAlmostEquals(
(0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
#(0,0,0) is always the original base in global coordinates #(0,0,0) is always the original base in global coordinates
self.assertTupleAlmostEquals(base.toTuple(), p.toWorldCoords((0,0)).toTuple() ,2 ) self.assertTupleAlmostEquals(
base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2)
def testXYPlaneOrigins(self): def testXYPlaneOrigins(self):
base = Vector(0,0,0.25) base = Vector(0, 0, 0.25)
p = Plane(base, Vector(1,0,0), Vector(0,0,1)) p = Plane(base, Vector(1, 0, 0), Vector(0, 0, 1))
#origin is always (0,0,0) in local coordinates # origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 ) self.assertTupleAlmostEquals(
(0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
#(0,0,0) is always the original base in global coordinates #(0,0,0) is always the original base in global coordinates
self.assertTupleAlmostEquals(toTuple(base), p.toWorldCoords((0,0)).toTuple() ,2 ) self.assertTupleAlmostEquals(
toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2)
def testXZPlaneOrigins(self): def testXZPlaneOrigins(self):
base = Vector(0,0.25,0) base = Vector(0, 0.25, 0)
p = Plane(base, Vector(0,0,1), Vector(0,1,0)) p = Plane(base, Vector(0, 0, 1), Vector(0, 1, 0))
#(0,0,0) is always the original base in global coordinates #(0,0,0) is always the original base in global coordinates
self.assertTupleAlmostEquals(toTuple(base), p.toWorldCoords((0,0)).toTuple() ,2 ) self.assertTupleAlmostEquals(
toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2)
#origin is always (0,0,0) in local coordinates # origin is always (0,0,0) in local coordinates
self.assertTupleAlmostEquals((0,0,0), p.toLocalCoords(p.origin).toTuple() ,2 ) self.assertTupleAlmostEquals(
(0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2)
def testPlaneBasics(self): def testPlaneBasics(self):
p = Plane.XY() p = Plane.XY()
#local to world # local to world
self.assertTupleAlmostEquals((1.0,1.0,0),p.toWorldCoords((1,1)).toTuple(),2 ) self.assertTupleAlmostEquals(
self.assertTupleAlmostEquals((-1.0,-1.0,0), p.toWorldCoords((-1,-1)).toTuple(),2 ) (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 # world to local
self.assertTupleAlmostEquals((-1.0,-1.0), p.toLocalCoords(Vector(-1,-1,0)).toTuple() ,2 ) self.assertTupleAlmostEquals(
self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(1,1,0)).toTuple() ,2 ) (-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() p = Plane.YZ()
self.assertTupleAlmostEquals((0,1.0,1.0),p.toWorldCoords((1,1)).toTuple() ,2 ) self.assertTupleAlmostEquals(
(0, 1.0, 1.0), p.toWorldCoords((1, 1)).toTuple(), 2)
#world to local # world to local
self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(0,1,1)).toTuple() ,2 ) self.assertTupleAlmostEquals(
(1.0, 1.0), p.toLocalCoords(Vector(0, 1, 1)).toTuple(), 2)
p = Plane.XZ() p = Plane.XZ()
r = p.toWorldCoords((1,1)).toTuple() r = p.toWorldCoords((1, 1)).toTuple()
self.assertTupleAlmostEquals((1.0,0.0,1.0),r ,2 ) self.assertTupleAlmostEquals((1.0, 0.0, 1.0), r, 2)
#world to local # world to local
self.assertTupleAlmostEquals((1.0,1.0), p.toLocalCoords(Vector(1,0,1)).toTuple() ,2 ) self.assertTupleAlmostEquals(
(1.0, 1.0), p.toLocalCoords(Vector(1, 0, 1)).toTuple(), 2)
def testOffsetPlanes(self): def testOffsetPlanes(self):
"Tests that a plane offset from the origin works ok too" "Tests that a plane offset from the origin works ok too"
p = Plane.XY(origin=(10.0,10.0,0)) 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)
self.assertTupleAlmostEquals((11.0,11.0,0.0),p.toWorldCoords((1.0,1.0)).toTuple(),2 ) # TODO test these offsets in the other dimensions too
self.assertTupleAlmostEquals((2.0,2.0), p.toLocalCoords(Vector(12.0,12.0,0)).toTuple() ,2 ) 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)
#TODO test these offsets in the other dimensions too p = Plane.XZ(origin=(2, 0, 2))
p = Plane.YZ(origin=(0,2,2)) r = p.toWorldCoords((1.0, 1.0)).toTuple()
self.assertTupleAlmostEquals((0.0,5.0,5.0), p.toWorldCoords((3.0,3.0)).toTuple() ,2 ) self.assertTupleAlmostEquals((3.0, 0.0, 3.0), r, 2)
self.assertTupleAlmostEquals((10,10.0,0.0), p.toLocalCoords(Vector(0.0,12.0,12.0)).toTuple() ,2 ) self.assertTupleAlmostEquals((10.0, 10.0), p.toLocalCoords(
Vector(12.0, 0.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 )
def testXYPlaneBasics(self): def testXYPlaneBasics(self):
p = Plane.named('XY') p = Plane.named('XY')

View File

@ -4,8 +4,9 @@ import unittest
import sys import sys
import os import os
def readFileAsString(fileName): def readFileAsString(fileName):
f= open(fileName, 'r') f = open(fileName, 'r')
s = f.read() s = f.read()
f.close() f.close()
return s return s
@ -37,13 +38,16 @@ def toTuple(v):
elif type(v) == Vector: elif type(v) == Vector:
return v.toTuple() return v.toTuple()
else: else:
raise RuntimeError("dont know how to convert type %s to tuple" % str(type(v)) ) raise RuntimeError(
"dont know how to convert type %s to tuple" % str(type(v)))
class BaseTest(unittest.TestCase): class BaseTest(unittest.TestCase):
def assertTupleAlmostEquals(self, expected, actual, places): def assertTupleAlmostEquals(self, expected, actual, places):
for i, j in zip(actual, expected): for i, j in zip(actual, expected):
self.assertAlmostEquals(i, j, places) self.assertAlmostEqual(i, j, places)
__all__ = ['TestCadObjects', 'TestCadQuery', 'TestCQSelectors', 'TestWorkplanes', 'TestExporters', 'TestCQSelectors', 'TestImporters','TestCQGI']
__all__ = ['TestCadObjects', 'TestCadQuery', 'TestCQSelectors', 'TestWorkplanes',
'TestExporters', 'TestCQSelectors', 'TestImporters', 'TestCQGI']