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
from .occ_impl.geom import Plane,BoundBox,Vector,Matrix
from .occ_impl.shapes import Shape,Vertex,Edge,Face,Wire,Solid,Shell,Compound,sortWiresByBuildOrder
# these items point to the OCC implementation
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 import exporters
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 .cq import *
__all__ = [
'CQ','Workplane','plugins','selectors','Plane','BoundBox','Matrix','Vector','sortWiresByBuildOrder',
'Shape','Vertex','Edge','Wire','Face','Solid','Shell','Compound','exporters', 'importers',
'NearestToPointSelector','ParallelDirSelector','DirectionSelector','PerpendicularDirSelector',
'TypeSelector','DirectionMinMaxSelector','StringSyntaxSelector','Selector','plugins'
'CQ', 'Workplane', 'plugins', 'selectors', 'Plane', 'BoundBox', 'Matrix', 'Vector', 'sortWiresByBuildOrder',
'Shape', 'Vertex', 'Edge', 'Wire', 'Face', 'Solid', 'Shell', 'Compound', 'exporters', 'importers',
'NearestToPointSelector', 'ParallelDirSelector', 'DirectionSelector', 'PerpendicularDirSelector',
'TypeSelector', 'DirectionMinMaxSelector', 'StringSyntaxSelector', 'Selector', 'plugins'
]
__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
which allows for shared state when needed,
"""
def __init__(self):
self.pendingWires = [] # a list of wires that have been created and need to be extruded
self.pendingEdges = [] # a list of 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.
# Used to determine how to behave when close() is called
self.firstPoint = None
@ -167,8 +169,8 @@ class CQ(object):
Most of the time, both objects will contain a single solid, which is
combined and returned on the stack of the new object.
"""
#loop through current stack objects, and combine them
#TODO: combine other types of objects as well, like edges and wires
# loop through current stack objects, and combine them
# TODO: combine other types of objects as well, like edges and wires
toCombine = self.solids().vals()
if otherCQToCombine:
@ -178,13 +180,13 @@ class CQ(object):
if len(toCombine) < 1:
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)
if ctxSolid is None:
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
for tc in toCombine:
s = s.fuse(tc)
@ -311,9 +313,9 @@ class CQ(object):
n1 = f1.normalAt()
# test normals (direction of planes)
if not ((abs(n0.x-n1.x) < self.ctx.tolerance) or
(abs(n0.y-n1.y) < self.ctx.tolerance) or
(abs(n0.z-n1.z) < self.ctx.tolerance)):
if not ((abs(n0.x - n1.x) < self.ctx.tolerance) or
(abs(n0.y - n1.y) < self.ctx.tolerance) or
(abs(n0.z - n1.z) < self.ctx.tolerance)):
return False
# test if p1 is on the plane of f0 (offset of planes)
@ -328,14 +330,15 @@ class CQ(object):
"""
xd = Vector(0, 0, 1).cross(normal)
if xd.Length < self.ctx.tolerance:
#this face is parallel with the x-y plane, so choose x to be in global coordinates
# this face is parallel with the x-y plane, so choose x to be in global coordinates
xd = Vector(1, 0, 0)
return xd
if len(self.objects) > 1:
# are all objects 'PLANE'?
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.")
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.")
# are all faces co-planar with each other?
if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]):
@ -368,23 +371,24 @@ class CQ(object):
normal = self.plane.zDir
xDir = self.plane.xDir
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:
normal = normal.multiply(-1.0)
#offset origin if desired
# offset origin if desired
offsetVector = normal.normalized().multiply(offset)
offsetCenter = center.add(offsetVector)
#make the new workplane
# make the new workplane
plane = Plane(offsetCenter, xDir, normal)
s = Workplane(plane)
s.parent = self
s.ctx = self.ctx
#a new workplane has the center of the workplane on the stack
# a new workplane has the center of the workplane on the stack
return s
def first(self):
@ -479,7 +483,7 @@ class CQ(object):
toReturn = self._collectProperty(objType)
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)
else:
selectorObj = selector
@ -716,7 +720,7 @@ class CQ(object):
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)
def _rot(obj):
@ -751,10 +755,10 @@ class CQ(object):
:param basePointVector: the base point to mirror about
:type basePointVector: tuple
"""
newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)])
newS = self.newObject(
[self.objects[0].mirror(mirrorPlane, basePointVector)])
return newS.first()
def translate(self, vec):
"""
Returns a copy of all of the items on the stack moved by the specified translation vector.
@ -765,7 +769,6 @@ class CQ(object):
"""
return self.newObject([o.translate(vec) for o in self.objects])
def shell(self, thickness):
"""
Remove the selected faces to create a shell of the specified thickness.
@ -935,7 +938,7 @@ class Workplane(CQ):
if inPlane.__class__.__name__ == 'Plane':
tmpPlane = inPlane
elif isinstance(inPlane, str) or isinstance(inPlane, unicode):
elif isinstance(inPlane, str) or isinstance(inPlane, str):
tmpPlane = Plane.named(inPlane, origin)
else:
tmpPlane = None
@ -964,7 +967,7 @@ class Workplane(CQ):
: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':
rotate = rotate.toTuple()
@ -990,7 +993,7 @@ class Workplane(CQ):
: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.plane = self.plane
ns.parent = self
@ -1026,7 +1029,8 @@ class Workplane(CQ):
elif isinstance(obj, Vector):
p = obj
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:
return self.plane.toLocalCoords(p)
@ -1055,10 +1059,10 @@ class Workplane(CQ):
for y in range(yCount):
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:
xc = xSpacing*(xCount-1) * 0.5
yc = ySpacing*(yCount-1) * 0.5
xc = xSpacing * (xCount - 1) * 0.5
yc = ySpacing * (yCount - 1) * 0.5
cpoints = []
for p in lpoints:
cpoints.append((p[0] - xc, p[1] - yc))
@ -1204,7 +1208,7 @@ class Workplane(CQ):
p = self._findFromPoint(True)
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):
"""
Move to the specified point, without drawing.
@ -1223,7 +1227,7 @@ class Workplane(CQ):
newCenter = Vector(x, y, 0)
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):
"""
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
"""
#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)
#attempt to consolidate wires together.
# attempt to consolidate wires together.
consolidated = n.consolidateWires()
rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix)
rotatedWires = self.plane.rotateShapes(
consolidated.wires().vals(), matrix)
for w in rotatedWires:
consolidated.objects.append(w)
consolidated._addPendingWire(w)
#attempt again to consolidate all of the wires
# attempt again to consolidate all of the wires
c = consolidated.consolidateWires()
return c
@ -1369,10 +1374,10 @@ class Workplane(CQ):
Future Enhancements:
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)
#attempt to consolidate wires together.
# attempt to consolidate wires together.
consolidated = n.consolidateWires()
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(),
@ -1382,7 +1387,7 @@ class Workplane(CQ):
consolidated.objects.append(w)
consolidated._addPendingWire(w)
#attempt again to consolidate all of the wires
# attempt again to consolidate all of the wires
return consolidated.consolidateWires()
def mirrorX(self):
@ -1399,10 +1404,10 @@ class Workplane(CQ):
Future Enhancements:
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)
#attempt to consolidate wires together.
# attempt to consolidate wires together.
consolidated = n.consolidateWires()
mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(),
@ -1412,7 +1417,7 @@ class Workplane(CQ):
consolidated.objects.append(w)
consolidated._addPendingWire(w)
#attempt again to consolidate all of the wires
# attempt again to consolidate all of the wires
return consolidated.consolidateWires()
def _addPendingEdge(self, edge):
@ -1458,15 +1463,15 @@ class Workplane(CQ):
if len(wires) < 2:
return self
#TODO: this makes the assumption that either all wires could be combined, or none.
#in reality trying each combination of wires is probably not reasonable anyway
# TODO: this makes the assumption that either all wires could be combined, or none.
# in reality trying each combination of wires is probably not reasonable anyway
w = Wire.combine(wires)
#ok this is a little tricky. if we consolidate wires, we have to actually
#modify the pendingWires collection to remove the original ones, and replace them
#with the consolidate done
#since we are already assuming that all wires could be consolidated, its easy, we just
#clear the pending wire list
# ok this is a little tricky. if we consolidate wires, we have to actually
# modify the pendingWires collection to remove the original ones, and replace them
# with the consolidate done
# since we are already assuming that all wires could be consolidated, its easy, we just
# clear the pending wire list
r = self.newObject([w])
r.ctx.pendingWires = []
r._addPendingWire(w)
@ -1495,7 +1500,7 @@ class Workplane(CQ):
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:
return self
@ -1506,7 +1511,6 @@ class Workplane(CQ):
if type(e) != Edge:
others.append(e)
w = Wire.assembleEdges(edges)
if not forConstruction:
self._addPendingWire(w)
@ -1549,7 +1553,7 @@ class Workplane(CQ):
for obj in self.objects:
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 = r.transformShape(self.plane.rG)
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
workplane/coordinate system
"""
#convert stack to a list of points
# convert stack to a list of points
pnts = []
if len(self.objects) == 0:
#nothing on the stack. here, we'll assume we should operate with the
#origin as the context point
# nothing on the stack. here, we'll assume we should operate with the
# origin as the context point
pnts.append(self.plane.origin)
else:
@ -1623,10 +1627,10 @@ class Workplane(CQ):
# Here pnt is in local coordinates due to useLocalCoords=True
# (xc,yc,zc) = pnt.toTuple()
if centered:
p1 = pnt.add(Vector(xLen/-2.0, yLen/-2.0, 0))
p2 = pnt.add(Vector(xLen/2.0, yLen/-2.0, 0))
p3 = pnt.add(Vector(xLen/2.0, yLen/2.0, 0))
p4 = pnt.add(Vector(xLen/-2.0, yLen/2.0, 0))
p1 = pnt.add(Vector(xLen / -2.0, yLen / -2.0, 0))
p2 = pnt.add(Vector(xLen / 2.0, yLen / -2.0, 0))
p3 = pnt.add(Vector(xLen / 2.0, yLen / 2.0, 0))
p4 = pnt.add(Vector(xLen / -2.0, yLen / 2.0, 0))
else:
p1 = pnt
p2 = pnt.add(Vector(xLen, 0, 0))
@ -1635,11 +1639,11 @@ class Workplane(CQ):
w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction)
return w
#return Part.makePolygon([p1,p2,p3,p4,p1])
# return Part.makePolygon([p1,p2,p3,p4,p1])
return self.eachpoint(makeRectangleWire, True)
#circle from current point
# circle from current point
def circle(self, radius, forConstruction=False):
"""
Make a circle for each item on the stack.
@ -1688,12 +1692,12 @@ class Workplane(CQ):
:return: a polygon wire
"""
def _makePolygon(center):
#pnt is a vector in local coordinates
# pnt is a vector in local coordinates
angle = 2.0 * math.pi / nSides
pnts = []
for i in range(nSides+1):
pnts.append(center + Vector((diameter / 2.0 * math.cos(angle*i)),
(diameter / 2.0 * math.sin(angle*i)), 0))
for i in range(nSides + 1):
pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)),
(diameter / 2.0 * math.sin(angle * i)), 0))
return Wire.makePolygon(pnts, forConstruction)
return self.eachpoint(_makePolygon, True)
@ -1749,7 +1753,7 @@ class Workplane(CQ):
self.lineTo(self.ctx.firstPoint.x, self.ctx.firstPoint.y)
# Need to reset the first point after closing a wire
self.ctx.firstPoint=None
self.ctx.firstPoint = None
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
: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: the time this works. but a stronger implementation would be to search all solids.
# 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.
s = self.findSolid()
if s:
return s.BoundingBox().DiagonalLength * 5.0
@ -1782,18 +1786,19 @@ class Workplane(CQ):
if ctxSolid is None:
raise ValueError("Must have a solid in the chain to cut from!")
#will contain all of the counterbores as a single compound
# will contain all of the counterbores as a single compound
results = self.eachpoint(fcn, useLocalCoords).vals()
s = ctxSolid
for cb in results:
s = s.cut(cb)
if clean: s = s.clean()
if clean:
s = s.clean()
ctxSolid.wrapped = s.wrapped
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):
"""
Makes a counterbored hole for each item on the stack.
@ -1834,18 +1839,20 @@ class Workplane(CQ):
pnt is in local coordinates
"""
boreDir = Vector(0, 0, -1)
#first make the hole
hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coordianates!
# first make the hole
hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coordianates!
#add the counter bore
cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir)
# add the counter bore
cbore = Solid.makeCylinder(
cboreDiameter / 2.0, cboreDepth, center, boreDir)
r = hole.fuse(cbore)
return r
return self.cutEach(_makeCbore, True, clean)
#TODO: almost all code duplicated!
#but parameter list is different so a simple function pointer wont work
# TODO: almost all code duplicated!
# but parameter list is different so a simple function pointer wont work
def cskHole(self, diameter, cskDiameter, cskAngle, depth=None, clean=True):
"""
Makes a countersunk hole for each item on the stack.
@ -1881,12 +1888,13 @@ class Workplane(CQ):
depth = self.largestDimension()
def _makeCsk(center):
#center is in local coordinates
# center is in local coordinates
boreDir = Vector(0, 0, -1)
#first make the hole
hole = Solid.makeCylinder(diameter/2.0, depth, center, boreDir) # local coords!
# first make the hole
hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coords!
r = cskDiameter / 2.0
h = r / math.tan(math.radians(cskAngle / 2.0))
csk = Solid.makeCone(r, 0.0, h, center, boreDir)
@ -1895,8 +1903,8 @@ class Workplane(CQ):
return self.cutEach(_makeCsk, True, clean)
#TODO: almost all code duplicated!
#but parameter list is different so a simple function pointer wont work
# TODO: almost all code duplicated!
# but parameter list is different so a simple function pointer wont work
def hole(self, diameter, depth=None, clean=True):
"""
Makes a hole for each item on the stack.
@ -1933,13 +1941,14 @@ class Workplane(CQ):
pnt is in local coordinates
"""
boreDir = Vector(0, 0, -1)
#first make the hole
hole = Solid.makeCylinder(diameter / 2.0, depth, center, boreDir) # local coordinates!
# first make the hole
hole = Solid.makeCylinder(
diameter / 2.0, depth, center, boreDir) # local coordinates!
return hole
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):
"""
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
:return: a CQ object with the resulting solid selected.
"""
#group wires together into faces based on which ones are inside the others
#result is a list of lists
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
# group wires together into faces based on which ones are inside the others
# result is a list of lists
wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires), self.plane, [])
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
# 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)
#one would think that fusing faces into a compound and then extruding would work,
#but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
#but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work
# one would think that fusing faces into a compound and then extruding would work,
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
# but then cutting it from the main solid fails with BRep_NotDone.
# the work around is to extrude each and then join the resulting solids, which seems to work
#underlying cad kernel can only handle simple bosses-- we'll aggregate them if there
# underlying cad kernel can only handle simple bosses-- we'll aggregate them if there
# are multiple sets
r = None
for ws in wireSets:
@ -1988,7 +1999,8 @@ class Workplane(CQ):
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean: newS = newS.clean()
if clean:
newS = newS.clean()
return newS
def extrude(self, distance, combine=True, clean=True, both=False):
@ -2016,13 +2028,15 @@ class Workplane(CQ):
perpendicular to the plane extrude to surface. this is quite tricky since the surface
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:
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean: newS = newS.clean()
if clean:
newS = newS.clean()
return newS
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,
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
#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
# 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)
else:
newS = self.newObject([r])
if clean: newS = newS.clean()
if clean:
newS = newS.clean()
return newS
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.
"""
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:
newS = self._combineWithBase(r)
else:
newS = self.newObject([r])
if clean: newS = newS.clean()
if clean:
newS = newS.clean()
return newS
def _combineWithBase(self, obj):
@ -2128,7 +2145,8 @@ class Workplane(CQ):
for ss in items:
s = s.fuse(ss)
if clean: s = s.clean()
if clean:
s = s.clean()
return self.newObject([s])
@ -2146,11 +2164,12 @@ class Workplane(CQ):
: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:
solids = toUnion.solids().vals()
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)
for s in solids:
newS = newS.fuse(s)
@ -2159,7 +2178,7 @@ class Workplane(CQ):
else:
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
solidRef = self.findSolid(searchStack=True, searchParents=True)
if combine and solidRef is not None:
@ -2168,7 +2187,8 @@ class Workplane(CQ):
else:
r = newS
if clean: r = r.clean()
if clean:
r = r.clean()
return self.newObject([r])
@ -2194,14 +2214,15 @@ class Workplane(CQ):
solidToCut = None
if type(toCut) == CQ or type(toCut) == Workplane:
solidToCut = toCut.val()
elif type(toCut) in (Solid,Compound):
elif type(toCut) in (Solid, Compound):
solidToCut = toCut
else:
raise ValueError("Cannot cut type '{}'".format(type(toCut)))
newS = solidRef.cut(solidToCut)
if clean: newS = newS.clean()
if clean:
newS = newS.clean()
if combine:
solidRef.wrapped = newS.wrapped
@ -2227,16 +2248,17 @@ class Workplane(CQ):
Future Enhancements:
Cut Up to Surface
"""
#first, make the object
# first, make the object
toCut = self._extrude(distanceToCut)
#now find a solid in the chain
# now find a solid in the chain
solidRef = self.findSolid()
s = solidRef.cut(toCut)
if clean: s = s.clean()
if clean:
s = s.clean()
solidRef.wrapped = s.wrapped
return self.newObject([s])
@ -2295,21 +2317,22 @@ class Workplane(CQ):
extrude along a profile (sweep)
"""
#group wires together into faces based on which ones are inside the others
#result is a list of lists
# group wires together into faces based on which ones are inside the others
# result is a list of lists
s = time.time()
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
#print "sorted wires in %d sec" % ( time.time() - s )
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires), self.plane, [])
# 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)
#one would think that fusing faces into a compound and then extruding would work,
#but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
#but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work
# one would think that fusing faces into a compound and then extruding would work,
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc)
# but then cutting it from the main solid fails with BRep_NotDone.
# the work around is to extrude each and then join the resulting solids, which seems to work
# underlying cad kernel can only handle simple bosses-- we'll aggregate them if there are
# multiple sets
@ -2334,7 +2357,8 @@ class Workplane(CQ):
toFuse.append(thisObj)
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)
return Compound.makeCompound(toFuse)
@ -2353,16 +2377,18 @@ class Workplane(CQ):
This method is a utility method, primarily for plugin and internal use.
"""
#We have to gather the wires to be revolved
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
# We have to gather the wires to be revolved
wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires), self.plane, [])
#Mark that all of the wires have been used to create a revolution
# Mark that all of the wires have been used to create a revolution
self.ctx.pendingWires = []
#Revolve the wires, make a compound out of them and then fuse them
# Revolve the wires, make a compound out of them and then fuse them
toFuse = []
for ws in wireSets:
thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd)
thisObj = Solid.revolve(
ws[0], ws[1:], angleDegrees, axisStart, axisEnd)
toFuse.append(thisObj)
return Compound.makeCompound(toFuse)
@ -2378,13 +2404,16 @@ class Workplane(CQ):
# group wires together into faces based on which ones are inside the others
# result is a list of lists
s = time.time()
wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), self.plane, [])
wireSets = sortWiresByBuildOrder(
list(self.ctx.pendingWires), self.plane, [])
# print "sorted wires in %d sec" % ( time.time() - s )
self.ctx.pendingWires = [] # now all of the wires have been used to create an extrusion
# now all of the wires have been used to create an extrusion
self.ctx.pendingWires = []
toFuse = []
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)
return Compound.makeCompound(toFuse)
@ -2447,11 +2476,11 @@ class Workplane(CQ):
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:
return boxes
else:
#combine everything
# combine everything
return self.union(boxes, clean=clean)
def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360,
@ -2547,5 +2576,6 @@ class Workplane(CQ):
try:
cleanObjects = [obj.clean() for obj in self.objects]
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)

View File

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

View File

@ -9,6 +9,7 @@ import cadquery
CQSCRIPT = "<cqscript>"
def parse(script_source):
"""
Parses the script as a model, and returns a model.
@ -55,12 +56,12 @@ class CQModel(object):
Parse the script, and populate variables that appear to be
overridable.
"""
#assumption here: we assume that variable declarations
#are only at the top level of the script. IE, we'll ignore any
#variable definitions at lower levels of the script
# assumption here: we assume that variable declarations
# are only at the top level of the script. IE, we'll ignore any
# variable definitions at lower levels of the script
#we dont want to use the visit interface because here we excplicitly
#want to walk only the top level of the tree.
# we dont want to use the visit interface because here we excplicitly
# want to walk only the top level of the tree.
assignment_finder = ConstantAssignmentFinder(self.metadata)
for node in self.ast_tree.body:
@ -104,22 +105,23 @@ class CQModel(object):
env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \
.add_entry("build_object", collector.build_object) \
.add_entry("debug", collector.debug) \
.add_entry("describe_parameter",collector.describe_parameter) \
.add_entry("describe_parameter", collector.describe_parameter) \
.build()
c = compile(self.ast_tree, CQSCRIPT, 'exec')
exec (c, env)
result.set_debug(collector.debugObjects )
exec(c, env)
result.set_debug(collector.debugObjects)
if collector.has_results():
result.set_success_result(collector.outputObjects)
else:
raise NoOutputError("Script did not call build_object-- no output available.")
except Exception, ex:
print "Error Executing Script:"
raise NoOutputError(
"Script did not call build_object-- no output available.")
except Exception as ex:
print("Error Executing Script:")
result.set_failure_result(ex)
traceback.print_exc()
print "Full Text of Script:"
print self.script_source
print("Full Text of Script:")
print(self.script_source)
end = time.clock()
result.buildTime = end - start
@ -128,9 +130,10 @@ class CQModel(object):
def set_param_values(self, params):
model_parameters = self.metadata.parameters
for k, v in params.iteritems():
for k, v in params.items():
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.set_value(v)
@ -147,6 +150,7 @@ class BuildResult(object):
If unsuccessful, the exception property contains a reference to
the stack trace that occurred.
"""
def __init__(self):
self.buildTime = None
self.results = []
@ -173,14 +177,15 @@ class ScriptMetadata(object):
Defines the metadata for a parsed CQ Script.
the parameters property is a dict of InputParameter objects.
"""
def __init__(self):
self.parameters = {}
def add_script_parameter(self, p):
self.parameters[p.name] = p
def add_parameter_description(self,name,description):
print 'Adding Parameter name=%s, desc=%s' % ( name, description )
def add_parameter_description(self, name, description):
print('Adding Parameter name=%s, desc=%s' % (name, description))
p = self.parameters[name]
p.desc = description
@ -212,6 +217,7 @@ class InputParameter:
provide additional metadata
"""
def __init__(self):
#: the default value for the variable.
@ -283,6 +289,7 @@ class ScriptCallback(object):
the build_object() method is exposed to CQ scripts, to allow them
to return objects to the execution environment
"""
def __init__(self):
self.outputObjects = []
self.debugObjects = []
@ -294,13 +301,13 @@ class ScriptCallback(object):
"""
self.outputObjects.append(shape)
def debug(self,obj,args={}):
def debug(self, obj, args={}):
"""
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.
"""
@ -315,16 +322,19 @@ class ScriptCallback(object):
def has_results(self):
return len(self.outputObjects) > 0
class DebugObject(object):
"""
Represents a request to debug an object
Object is the type of object we want to debug
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.object = object
class InvalidParameterError(Exception):
"""
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 other methods the script will need.
"""
def __init__(self):
self.env = {}
@ -397,31 +408,34 @@ class EnvironmentBuilder(object):
def build(self):
return self.env
class ParameterDescriptionFinder(ast.NodeTransformer):
"""
Visits a parse tree, looking for function calls to describe_parameter(var, description )
"""
def __init__(self, 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?
"""
try:
if node.func.id == 'describe_parameter':
#looks like we have a call to our function.
#first parameter is the variable,
#second is the description
# looks like we have a call to our function.
# first parameter is the variable,
# second is the description
varname = node.args[0].id
desc = node.args[1].s
self.cqModel.add_parameter_description(varname,desc)
self.cqModel.add_parameter_description(varname, desc)
except:
print "Unable to handle function call"
print("Unable to handle function call")
pass
return node
class ConstantAssignmentFinder(ast.NodeTransformer):
"""
Visits a parse tree, and adds script parameters to the cqModel
@ -447,7 +461,7 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
self.cqModel.add_script_parameter(
InputParameter.create(value_node, var_name, BooleanParameterType, True))
except:
print "Unable to handle assignment for variable '%s'" % var_name
print("Unable to handle assignment for variable '%s'" % var_name)
pass
def visit_Assign(self, node):
@ -455,8 +469,8 @@ class ConstantAssignmentFinder(ast.NodeTransformer):
try:
left_side = node.targets[0]
#do not handle attribute assignments
if isinstance(left_side,ast.Attribute):
# do not handle attribute assignments
if isinstance(left_side, ast.Attribute):
return
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)
except:
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

View File

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

View File

@ -9,9 +9,11 @@ import os
import urllib as urlreader
import tempfile
class ImportTypes:
STEP = "STEP"
class UNITS:
MM = "mm"
IN = "in"
@ -24,23 +26,23 @@ def importShape(importType, fileName):
: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:
return importStep(fileName)
#Loads a STEP file into a CQ.Workplane object
# Loads a STEP file into a CQ.Workplane object
def importStep(fileName):
"""
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
"""
#Now read and return the shape
# Now read and return the shape
try:
#print fileName
# print fileName
rshape = Part.read(fileName)
#Make sure that we extract all the solids
# Make sure that we extract all the solids
solids = []
for solid in rshape.Solids:
solids.append(Shape.cast(solid))
@ -49,9 +51,11 @@ def importStep(fileName):
except:
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
# Now read and return the shape
try:
webFile = urlreader.urlopen(url)
tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False)
@ -61,11 +65,12 @@ def importStepFromURL(url):
rshape = Part.read(tempFile.name)
#Make sure that we extract all the solids
# Make sure that we extract all the solids
solids = []
for solid in rshape.Solids:
solids.append(Shape.cast(solid))
return cadquery.Workplane("XY").newObject(solids)
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':
tr = Solid(obj)
elif s == 'Compound':
#compound of solids, lets return a solid instead
# compound of solids, lets return a solid instead
if len(obj.Solids) > 1:
tr = Solid(obj)
elif len(obj.Solids) == 1:
@ -122,7 +122,7 @@ class Shape(object):
self.wrapped.exportStep(fileName)
elif fileFormat == ExportFormats.AMF:
# not built into FreeCAD
#TODO: user selected tolerance
# TODO: user selected tolerance
tess = self.wrapped.tessellate(0.1)
aw = amfUtils.AMFWriter(tess)
aw.writeAmf(fileName)
@ -189,7 +189,7 @@ class Shape(object):
return BoundBox(self.wrapped.BoundBox)
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)
elif mirrorPlane == "XZ" or mirrorPlane == "ZX":
mirrorPlaneNormalVector = FreeCAD.Base.Vector(0, 1, 0)
@ -214,9 +214,10 @@ class Shape(object):
elif isinstance(self.wrapped, FreeCADPart.Solid):
return Vector(self.wrapped.CenterOfMass)
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)
if isinstance(self.wrapped, FreeCADPart.Shape):
# 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):
return Vector(self.wrapped.BoundBox.Center)
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
def CombinedCenter(objects):
@ -239,13 +241,14 @@ class Shape(object):
:param objects: a list of objects with mass
"""
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]
for wc in weighted_centers[1:] :
for wc in weighted_centers[1:]:
sum_wc = sum_wc.add(wc)
return Vector(sum_wc.multiply(1./total_mass))
return Vector(sum_wc.multiply(1. / total_mass))
@staticmethod
def computeMass(object):
@ -259,7 +262,7 @@ class Shape(object):
return object.wrapped.Mass
@staticmethod
def CombinedCenterOfBoundBox(objects, tolerance = 0.1):
def CombinedCenterOfBoundBox(objects, tolerance=0.1):
"""
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))
sum_wc = weighted_centers[0]
for wc in weighted_centers[1:] :
for wc in weighted_centers[1:]:
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):
return self.wrapped.Closed
@ -430,7 +433,7 @@ class Edge(Shape):
def geomType(self):
t = type(self.wrapped.Curve)
if self.edgetypes.has_key(t):
if t in self.edgetypes:
return self.edgetypes[t]
else:
return "Unknown Edge Curve Type: %s" % str(t)
@ -565,7 +568,8 @@ class Wire(Shape):
:param normal: vector representing the direction of the plane the circle should lie in
: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
@classmethod
@ -588,10 +592,12 @@ class Wire(Shape):
"""This method is not implemented yet."""
return self
class Face(Shape):
"""
a bounded surface that represents part of the boundary of a solid
"""
def __init__(self, obj):
self.wrapped = obj
@ -608,7 +614,7 @@ class Face(Shape):
def geomType(self):
t = type(self.wrapped.Surface)
if self.facetypes.has_key(t):
if t in self.facetypes:
return self.facetypes[t]
else:
return "Unknown Face Surface Type: %s" % str(t)
@ -661,6 +667,7 @@ class Shell(Shape):
"""
the outer boundary of a surface
"""
def __init__(self, wrapped):
"""
A Shell
@ -679,6 +686,7 @@ class Solid(Shape):
"""
a single solid
"""
def __init__(self, obj):
"""
A Solid
@ -817,11 +825,11 @@ class Solid(Shape):
rs = FreeCADPart.makeRuledSurface(w1, w2)
sides.append(rs)
#make faces for the top and bottom
# make faces for the top and bottom
startFace = FreeCADPart.Face(startWires)
endFace = FreeCADPart.Face(endWires)
#collect all the faces from the sides
# collect all the faces from the sides
faceList = [startFace]
for s in sides:
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,
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
# but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work
# 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]
for w in innerWires:
freeCADWires.append(w.wrapped)
@ -906,10 +914,10 @@ class Solid(Shape):
rotateCenter = FreeCAD.Base.Vector(axisStart)
rotateAxis = FreeCAD.Base.Vector(axisEnd)
#Convert our axis end vector into to something FreeCAD will understand (an axis specification vector)
# Convert our axis end vector into to something FreeCAD will understand (an axis specification vector)
rotateAxis = rotateCenter.sub(rotateAxis)
#FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation
# FreeCAD wants a rotation center and then an axis to rotate around rather than an axis of rotation
result = f.revolve(rotateCenter, rotateAxis, angleDegrees)
return Shape.cast(result)

View File

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

View File

@ -21,6 +21,7 @@ class Vector(object):
* a 3-tuple
* three float values, x, y, and z
"""
def __init__(self, *args):
if len(args) == 3:
fV = gp_Vec(*args)
@ -40,7 +41,7 @@ class Vector(object):
else:
fV = args[0]
elif len(args) == 0:
fV = gp_Vec(0,0,0)
fV = gp_Vec(0, 0, 0)
else:
raise ValueError("Expected three floats, OCC Geom_, or 3-tuple")
@ -104,16 +105,20 @@ class Vector(object):
return self.wrapped.Angle(v.wrapped)
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):
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):
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):
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):
return self.add(v)
@ -122,10 +127,10 @@ class Vector(object):
return self.sub(v)
def __repr__(self):
return 'Vector: ' + str((self.x,self.y,self.z))
return 'Vector: ' + str((self.x, self.y, self.z))
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):
return self.wrapped == other.wrapped
@ -143,9 +148,9 @@ class Vector(object):
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_t = pnt.Transformed(T.wrapped)
@ -157,6 +162,7 @@ class Matrix:
Used to move geometry in space.
"""
def __init__(self, matrix=None):
if matrix is None:
self.wrapped = gp_Trsf()
@ -175,7 +181,7 @@ class Matrix:
self._rotate(gp.OZ(),
angle)
def _rotate(self,direction,angle):
def _rotate(self, direction, angle):
new = gp_Trsf()
new.SetRotation(direction,
@ -250,7 +256,7 @@ class Plane(object):
return namedPlanes[stdName]
except KeyError:
raise ValueError('Supported names are {}'.format(
namedPlanes.keys()))
list(namedPlanes.keys())))
@classmethod
def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)):
@ -353,6 +359,7 @@ class Plane(object):
def origin(self):
return self._origin
# TODO is this property rly needed -- why not handle this in the constructor
@origin.setter
def origin(self, value):
self._origin = Vector(value)
@ -511,7 +518,7 @@ class Plane(object):
# - then rotate about x
# - then transform back to global coordinates.
#TODO why is it here?
# TODO why is it here?
raise NotImplementedError
@ -543,7 +550,7 @@ class Plane(object):
return resultWires'''
def mirrorInPlane(self, listOfShapes, axis = 'X'):
def mirrorInPlane(self, listOfShapes, axis='X'):
local_coord_system = gp_Ax3(self.origin.toPnt(),
self.zDir.toDir(),
@ -563,7 +570,7 @@ class Plane(object):
for w in listOfShapes:
mirrored = w.transformShape(Matrix(T))
#attemp stitching of the wires
# attemp stitching of the wires
resultWires.append(mirrored)
return resultWires
@ -599,15 +606,15 @@ class Plane(object):
inverse.wrapped.SetTransformation(local_coord_system,
global_coord_system)
#TODO verify if this is OK
# TODO verify if this is OK
self.lcs = local_coord_system
self.rG = inverse
self.fG = forward
class BoundBox(object):
"""A BoundingBox for an object or set of objects. Wraps the OCC one"""
def __init__(self, bb):
self.wrapped = bb
XMin, YMin, ZMin, XMax, YMax, ZMax = bb.Get()
@ -622,9 +629,9 @@ class BoundBox(object):
self.zmax = ZMax
self.zlen = ZMax - ZMin
self.center = Vector((XMax+XMin)/2,
(YMax+YMin)/2,
(ZMax+ZMin)/2)
self.center = Vector((XMax + XMin) / 2,
(YMax + YMin) / 2,
(ZMax + ZMin) / 2)
self.DiagonalLength = self.wrapped.SquareExtent()**0.5
@ -680,7 +687,7 @@ class BoundBox(object):
return None
@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
'''
@ -688,11 +695,12 @@ class BoundBox(object):
bbox.SetGap(tol)
if optimal:
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:
mesh = BRepMesh_IncrementalMesh(shape,TOL,True)
mesh = BRepMesh_IncrementalMesh(shape, TOL, True)
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)

View File

@ -9,9 +9,11 @@ import tempfile
from OCC.STEPControl import STEPControl_Reader
class ImportTypes:
STEP = "STEP"
class UNITS:
MM = "mm"
IN = "in"
@ -24,18 +26,18 @@ def importShape(importType, fileName):
: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:
return importStep(fileName)
#Loads a STEP file into a CQ.Workplane object
# Loads a STEP file into a CQ.Workplane object
def importStep(fileName):
"""
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
"""
#Now read and return the shape
# Now read and return the shape
try:
reader = STEPControl_Reader()
reader.ReadFile(fileName)
@ -43,9 +45,9 @@ def importStep(fileName):
occ_shapes = []
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 = []
for shape in occ_shapes:
solids.append(Shape.cast(shape))
@ -54,9 +56,11 @@ def importStep(fileName):
except:
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
# Now read and return the shape
try:
webFile = urlreader.urlopen(url)
tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False)
@ -66,4 +70,5 @@ def importStepFromURL(url):
return importStep(tempFile.name)
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

@ -1,12 +1,13 @@
from cadquery import Vector, BoundBox
import OCC.TopAbs as ta #Tolopolgy type enum
import OCC.GeomAbs as ga #Geometry type enum
import OCC.TopAbs as ta # Tolopolgy type enum
import OCC.GeomAbs as ga # Geometry type enum
from OCC.gp import (gp_Vec, gp_Pnt,gp_Ax1, gp_Ax2, gp_Ax3, gp_Dir, gp_Circ,
from OCC.gp import (gp_Vec, gp_Pnt, gp_Ax1, gp_Ax2, gp_Ax3, gp_Dir, gp_Circ,
gp_Trsf, gp_Pln, gp_GTrsf, gp_Pnt2d, gp_Dir2d)
from OCC.TColgp import TColgp_Array1OfPnt #collection of pints (used for spline construction)
# collection of pints (used for spline construction)
from OCC.TColgp import TColgp_Array1OfPnt
from OCC.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_MakeEdge,
@ -16,14 +17,15 @@ from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex,
BRepBuilderAPI_Copy,
BRepBuilderAPI_GTransform,
BRepBuilderAPI_Transform)
from OCC.GProp import GProp_GProps #properties used to store mass calculation result
# properties used to store mass calculation result
from OCC.GProp import GProp_GProps
from OCC.BRepGProp import BRepGProp_Face, \
brepgprop_LinearProperties, \
brepgprop_SurfaceProperties, \
brepgprop_VolumeProperties #used for mass calculation
from OCC.BRepLProp import BRepLProp_CLProps #local curve properties
brepgprop_VolumeProperties # used for mass calculation
from OCC.BRepLProp import BRepLProp_CLProps # local curve properties
from OCC.BRepPrimAPI import (BRepPrimAPI_MakeBox, #TODO list functions/used for making primitives
from OCC.BRepPrimAPI import (BRepPrimAPI_MakeBox, # TODO list functions/used for making primitives
BRepPrimAPI_MakeCone,
BRepPrimAPI_MakeCylinder,
BRepPrimAPI_MakeTorus,
@ -32,12 +34,13 @@ from OCC.BRepPrimAPI import (BRepPrimAPI_MakeBox, #TODO list functions/used for
BRepPrimAPI_MakeRevol,
BRepPrimAPI_MakeSphere)
from OCC.TopExp import TopExp_Explorer #Toplogy explorer
from OCC.BRepTools import (BRepTools_WireExplorer, #might be needed for iterating thorugh wires
from OCC.TopExp import TopExp_Explorer # Toplogy explorer
from OCC.BRepTools import (BRepTools_WireExplorer, # might be needed for iterating thorugh wires
breptools_UVBounds)
from OCC.BRep import BRep_Tool #used for getting underlying geoetry -- is this equvalent to brep adaptor?
# used for getting underlying geoetry -- is this equvalent to brep adaptor?
from OCC.BRep import BRep_Tool
from OCC.TopoDS import (topods_Vertex, #downcasting functions
from OCC.TopoDS import (topods_Vertex, # downcasting functions
topods_Edge,
topods_Wire,
topods_Face,
@ -49,7 +52,7 @@ from OCC.TopoDS import (TopoDS_Shell,
TopoDS_Compound,
TopoDS_Builder)
from OCC.GC import GC_MakeArcOfCircle #geometry construction
from OCC.GC import GC_MakeArcOfCircle # geometry construction
from OCC.GCE2d import GCE2d_MakeSegment
from OCC.GeomAPI import (GeomAPI_PointsToBSpline,
GeomAPI_ProjectPointOnSurf)
@ -100,56 +103,56 @@ from OCC.BRepTools import breptools_Write
from math import pi, sqrt
TOLERANCE = 1e-6
DEG2RAD = 2*pi / 360.
HASH_CODE_MAX = int(1e+6) #required by OCC HashCode
DEG2RAD = 2 * pi / 360.
HASH_CODE_MAX = int(1e+6) # required by OCC HashCode
shape_LUT = \
{ta.TopAbs_VERTEX : 'Vertex',
ta.TopAbs_EDGE : 'Edge',
ta.TopAbs_WIRE : 'Wire',
ta.TopAbs_FACE : 'Face',
ta.TopAbs_SHELL : 'Shell',
ta.TopAbs_SOLID : 'Solid',
ta.TopAbs_COMPOUND : 'Compound'}
{ta.TopAbs_VERTEX: 'Vertex',
ta.TopAbs_EDGE: 'Edge',
ta.TopAbs_WIRE: 'Wire',
ta.TopAbs_FACE: 'Face',
ta.TopAbs_SHELL: 'Shell',
ta.TopAbs_SOLID: 'Solid',
ta.TopAbs_COMPOUND: 'Compound'}
shape_properties_LUT = \
{ta.TopAbs_VERTEX : None,
ta.TopAbs_EDGE : brepgprop_LinearProperties,
ta.TopAbs_WIRE : brepgprop_LinearProperties,
ta.TopAbs_FACE : brepgprop_SurfaceProperties,
ta.TopAbs_SHELL : brepgprop_SurfaceProperties,
ta.TopAbs_SOLID : brepgprop_VolumeProperties,
ta.TopAbs_COMPOUND : brepgprop_VolumeProperties}
{ta.TopAbs_VERTEX: None,
ta.TopAbs_EDGE: brepgprop_LinearProperties,
ta.TopAbs_WIRE: brepgprop_LinearProperties,
ta.TopAbs_FACE: brepgprop_SurfaceProperties,
ta.TopAbs_SHELL: brepgprop_SurfaceProperties,
ta.TopAbs_SOLID: brepgprop_VolumeProperties,
ta.TopAbs_COMPOUND: brepgprop_VolumeProperties}
inverse_shape_LUT = {v:k for k,v in shape_LUT.iteritems()}
inverse_shape_LUT = {v: k for k, v in shape_LUT.items()}
downcast_LUT = \
{ta.TopAbs_VERTEX : topods_Vertex,
ta.TopAbs_EDGE : topods_Edge,
ta.TopAbs_WIRE : topods_Wire,
ta.TopAbs_FACE : topods_Face,
ta.TopAbs_SHELL : topods_Shell,
ta.TopAbs_SOLID : topods_Solid,
ta.TopAbs_COMPOUND : topods_Compound}
{ta.TopAbs_VERTEX: topods_Vertex,
ta.TopAbs_EDGE: topods_Edge,
ta.TopAbs_WIRE: topods_Wire,
ta.TopAbs_FACE: topods_Face,
ta.TopAbs_SHELL: topods_Shell,
ta.TopAbs_SOLID: topods_Solid,
ta.TopAbs_COMPOUND: topods_Compound}
geom_LUT = \
{ta.TopAbs_VERTEX : 'Vertex',
ta.TopAbs_EDGE : BRepAdaptor_Curve,
ta.TopAbs_WIRE : 'Wire',
ta.TopAbs_FACE : BRepAdaptor_Surface,
ta.TopAbs_SHELL : 'Shell',
ta.TopAbs_SOLID : 'Solid',
ta.TopAbs_COMPOUND : 'Compound'}
{ta.TopAbs_VERTEX: 'Vertex',
ta.TopAbs_EDGE: BRepAdaptor_Curve,
ta.TopAbs_WIRE: 'Wire',
ta.TopAbs_FACE: BRepAdaptor_Surface,
ta.TopAbs_SHELL: 'Shell',
ta.TopAbs_SOLID: 'Solid',
ta.TopAbs_COMPOUND: 'Compound'}
#TODO there are many more geometry types, what to do with those?
# TODO there are many more geometry types, what to do with those?
geom_LUT_EDGE_FACE = \
{ga.GeomAbs_Arc : 'ARC',
ga.GeomAbs_Circle : 'CIRCLE',
ga.GeomAbs_Line : 'LINE',
ga.GeomAbs_BSplineCurve : 'SPLINE', #BSpline or Bezier?
ga.GeomAbs_Plane : 'PLANE',
ga.GeomAbs_Sphere : 'SPHERE',
ga.GeomAbs_Cone : 'CONE',
{ga.GeomAbs_Arc: 'ARC',
ga.GeomAbs_Circle: 'CIRCLE',
ga.GeomAbs_Line: 'LINE',
ga.GeomAbs_BSplineCurve: 'SPLINE', # BSpline or Bezier?
ga.GeomAbs_Plane: 'PLANE',
ga.GeomAbs_Sphere: 'SPHERE',
ga.GeomAbs_Cone: 'CONE',
}
@ -160,6 +163,7 @@ def downcast(topods_obj):
return downcast_LUT[topods_obj.ShapeType()](topods_obj)
class Shape(object):
"""
Represents a shape in the system.
@ -173,11 +177,11 @@ class Shape(object):
# Helps identify this solid through the use of an ID
self.label = ""
def clean(self):
"""Experimental clean using ShapeUpgrade"""
upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped,True,True,False)
upgrader = ShapeUpgrade_UnifySameDomain(
self.wrapped, True, True, False)
upgrader.Build()
return self.cast(upgrader.Shape())
@ -188,22 +192,23 @@ class Shape(object):
'''
if type(obj) == FreeCAD.Base.Vector:
return Vector(obj)
''' #FIXME to be removed?
''' # FIXME to be removed?
tr = None
#define the shape lookup table for casting
constructor_LUT = {ta.TopAbs_VERTEX : Vertex,
ta.TopAbs_EDGE : Edge,
ta.TopAbs_WIRE : Wire,
ta.TopAbs_FACE : Face,
ta.TopAbs_SHELL : Shell,
ta.TopAbs_SOLID : Solid,
ta.TopAbs_COMPOUND : Compound}
# define the shape lookup table for casting
constructor_LUT = {ta.TopAbs_VERTEX: Vertex,
ta.TopAbs_EDGE: Edge,
ta.TopAbs_WIRE: Wire,
ta.TopAbs_FACE: Face,
ta.TopAbs_SHELL: Shell,
ta.TopAbs_SOLID: Solid,
ta.TopAbs_COMPOUND: Compound}
t = obj.ShapeType()
tr = constructor_LUT[t](downcast(obj)) #NB downcast is nedded to handly TopoDS_Shape types
# NB downcast is nedded to handly TopoDS_Shape types
tr = constructor_LUT[t](downcast(obj))
tr.forConstruction = forConstruction
#TODO move this to Compound constructor?
# TODO move this to Compound constructor?
'''
#compound of solids, lets return a solid instead
if len(obj.Solids) > 1:
@ -223,15 +228,14 @@ class Shape(object):
# TODO: all these should move into the exporters folder.
# we dont need a bunch of exporting code stored in here!
#
def exportStl(self, fileName, precision = 1e-5):
def exportStl(self, fileName, precision=1e-5):
mesh = BRepMesh_IncrementalMesh(self.wrapped,precision,True)
mesh = BRepMesh_IncrementalMesh(self.wrapped, precision, True)
mesh.Perform()
writer = StlAPI_Writer()
return writer.Write(self.wrapped,fileName)
return writer.Write(self.wrapped, fileName)
def exportStep(self, fileName):
@ -282,8 +286,7 @@ class Shape(object):
else:
return geom_LUT_EDGE_FACE[tr(self.wrapped).GetType()]
def isType(self, obj, strType): #TODO why here?
def isType(self, obj, strType): # TODO why here?
"""
Returns True if the shape is the specified type, false otherwise
@ -307,15 +310,15 @@ class Shape(object):
def isEqual(self, other):
return self.wrapped.IsEqual(other.wrapped)
def isValid(self): #seems to be not used in the codebase -- remove?
def isValid(self): # seems to be not used in the codebase -- remove?
raise NotImplemented
def BoundingBox(self, tolerance=0.1): #need to implement that in GEOM
def BoundingBox(self, tolerance=0.1): # need to implement that in GEOM
return BoundBox._fromTopoDS(self.wrapped)
def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)):
if mirrorPlane == "XY" or mirrorPlane== "YX":
if mirrorPlane == "XY" or mirrorPlane == "YX":
mirrorPlaneNormalVector = gp_Vec(0, 0, 1)
elif mirrorPlane == "XZ" or mirrorPlane == "ZX":
mirrorPlaneNormalVector = gp_Vec(0, 1, 0)
@ -347,24 +350,25 @@ class Shape(object):
return Shape.centerOfMass(self)
def CenterOfBoundBox(self, tolerance = 0.1):
def CenterOfBoundBox(self, tolerance=0.1):
return self.BoundingBox(self.wrapped).center
@staticmethod
def CombinedCenter(objects): #TODO
def CombinedCenter(objects): # TODO
"""
Calculates the center of mass of multiple objects.
:param objects: a list of objects with mass
"""
total_mass = sum(Shape.computeMass(o) for o in objects)
weighted_centers = [Shape.centerOfMass(o).multiply(Shape.computeMass(o)) for o in objects]
weighted_centers = [Shape.centerOfMass(o).multiply(
Shape.computeMass(o)) for o in objects]
sum_wc = weighted_centers[0]
for wc in weighted_centers[1:] :
for wc in weighted_centers[1:]:
sum_wc = sum_wc.add(wc)
return Vector(sum_wc.multiply(1./total_mass))
return Vector(sum_wc.multiply(1. / total_mass))
@staticmethod
def computeMass(obj):
@ -375,7 +379,7 @@ class Shape(object):
calc_function = shape_properties_LUT[obj.wrapped.ShapeType()]
if calc_function:
calc_function(obj.wrapped,Properties)
calc_function(obj.wrapped, Properties)
return Properties.Mass()
else:
raise NotImplemented
@ -389,13 +393,13 @@ class Shape(object):
calc_function = shape_properties_LUT[obj.wrapped.ShapeType()]
if calc_function:
calc_function(obj.wrapped,Properties)
calc_function(obj.wrapped, Properties)
return Vector(Properties.CentreOfMass())
else:
raise NotImplemented
@staticmethod
def CombinedCenterOfBoundBox(objects, tolerance = 0.1): #TODO
def CombinedCenterOfBoundBox(objects, tolerance=0.1): # TODO
"""
Calculates the center of BoundBox of multiple objects.
@ -409,10 +413,10 @@ class Shape(object):
weighted_centers.append(o.wrapped.BoundBox.Center.multiply(1.0))
sum_wc = weighted_centers[0]
for wc in weighted_centers[1:] :
for wc in weighted_centers[1:]:
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):
return self.wrapped.Closed()
@ -420,10 +424,9 @@ class Shape(object):
def ShapeType(self):
return shape_LUT[self.wrapped.ShapeType()]
def _entities(self, topo_type):
def _entities(self,topo_type):
out = {} #using dict to prevent duplicates
out = {} # using dict to prevent duplicates
explorer = TopExp_Explorer(self.wrapped, inverse_shape_LUT[topo_type])
@ -432,7 +435,7 @@ class Shape(object):
out[item.__hash__()] = item # some implementations use __hash__
explorer.Next()
return out.values()
return list(out.values())
def Vertices(self):
@ -459,7 +462,7 @@ class Shape(object):
def Area(self):
raise NotImplementedError
def _apply_transform(self,T):
def _apply_transform(self, T):
return Shape.cast(BRepBuilderAPI_Transform(self.wrapped,
T,
@ -556,10 +559,10 @@ class Shape(object):
Fuse shapes together
"""
fuse_op = BRepAlgoAPI_Fuse(self.wrapped,toFuse.wrapped)
fuse_op = BRepAlgoAPI_Fuse(self.wrapped, toFuse.wrapped)
fuse_op.RefineEdges()
fuse_op.FuseEdges()
#fuse_op.SetFuzzyValue(TOLERANCE)
# fuse_op.SetFuzzyValue(TOLERANCE)
fuse_op.Build()
return Shape.cast(fuse_op.Shape())
@ -578,6 +581,7 @@ class Shape(object):
raise NotImplemented
class Vertex(Shape):
"""
A Single Point in Space
@ -587,7 +591,7 @@ class Vertex(Shape):
"""
Create a vertex from a FreeCAD Vertex
"""
super(Vertex,self).__init__(obj)
super(Vertex, self).__init__(obj)
self.forConstruction = forConstruction
self.X, self.Y, self.Z = self.toTuple()
@ -606,9 +610,9 @@ class Vertex(Shape):
return Vector(self.toTuple())
@classmethod
def makeVertex(cls,x,y,z):
def makeVertex(cls, x, y, z):
return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x,y,z)
return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)
).Vertex())
@ -617,10 +621,11 @@ class Mixin1D(object):
def Length(self):
Properties = GProp_GProps()
brepgprop_LinearProperties(self.wrapped,Properties)
brepgprop_LinearProperties(self.wrapped, Properties)
return Properties.Mass()
class Edge(Shape, Mixin1D):
"""
A trimmed curve that represents the border of a face
@ -632,7 +637,6 @@ class Edge(Shape, Mixin1D):
"""
return BRepAdaptor_Curve(self.wrapped)
def startPoint(self):
"""
@ -673,13 +677,14 @@ class Edge(Shape, Mixin1D):
raise NotImplementedError
else:
umin, umax = curve.FirstParameter(), curve.LastParameter()
umid = 0.5*(umin+umax)
umid = 0.5 * (umin + umax)
curve_props = BRepLProp_CLProps(curve, 2, curve.Tolerance()) #TODO what are good parameters for those?
# TODO what are good parameters for those?
curve_props = BRepLProp_CLProps(curve, 2, curve.Tolerance())
curve_props.SetParameter(umid)
if curve_props.IsTangentDefined():
dir_handle = gp_Dir() #this is awkward due to C++ pass by ref in the API
dir_handle = gp_Dir() # this is awkward due to C++ pass by ref in the API
curve_props.Tangent(dir_handle)
return Vector(dir_handle)
@ -704,12 +709,12 @@ class Edge(Shape, Mixin1D):
dir.toDir()),
radius)
if angle1 == angle2: #full circle case
if angle1 == angle2: # full circle case
return cls(BRepBuilderAPI_MakeEdge(circle_gp).Edge())
else: #arc case
else: # arc case
circle_geom = GC_MakeArcOfCircle(circle_gp,
angle1*DEG2RAD,
angle2*DEG2RAD,
angle1 * DEG2RAD,
angle2 * DEG2RAD,
True).Value()
return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge())
@ -721,8 +726,9 @@ class Edge(Shape, Mixin1D):
:param listOfVector: a list of Vectors that represent the points
:return: an Edge
"""
pnts = TColgp_Array1OfPnt(0,len(listOfVector)-1)
for ix,v in enumerate(listOfVector): pnts.SetValue(ix,v.toPnt())
pnts = TColgp_Array1OfPnt(0, len(listOfVector) - 1)
for ix, v in enumerate(listOfVector):
pnts.SetValue(ix, v.toPnt())
spline_geom = GeomAPI_PointsToBSpline(pnts).Curve()
@ -801,7 +807,7 @@ class Wire(Shape, Mixin1D):
:return:
"""
circle_edge = Edge.makeCircle(radius,center,normal)
circle_edge = Edge.makeCircle(radius, center, normal)
w = cls.assembleEdges([circle_edge])
return w
@ -810,7 +816,8 @@ class Wire(Shape, Mixin1D):
# convert list of tuples into Vectors.
wire_builder = BRepBuilderAPI_MakePolygon()
for v in listOfVertices: wire_builder.Add(v.toPnt())
for v in listOfVertices:
wire_builder.Add(v.toPnt())
w = cls(wire_builder.Wire())
w.forConstruction = forConstruction
@ -818,40 +825,40 @@ class Wire(Shape, Mixin1D):
return w
@classmethod
def makeHelix(cls, pitch, height, radius, center=Vector(0,0,0), dir=Vector(0,0,1), angle=360.0):
def makeHelix(cls, pitch, height, radius, center=Vector(0, 0, 0), dir=Vector(0, 0, 1), angle=360.0):
"""
Make a helix with a given pitch, height and radius
By default a cylindrical surface is used to create the helix. If
the fourth parameter is set (the apex given in degree) a conical surface is used instead'
"""
#1. build underlying cylindrical/conical surface
# 1. build underlying cylindrical/conical surface
if angle == 360.:
geom_surf = Geom_CylindricalSurface(gp_Ax3(center.toPnt(),dir.toDir()),
geom_surf = Geom_CylindricalSurface(gp_Ax3(center.toPnt(), dir.toDir()),
radius)
else:
geom_surf = Geom_ConicalSurface(gp_Ax3(center.toPnt(),dir.toDir()),
angle*DEG2RAD,#TODO why no orientation?
geom_surf = Geom_ConicalSurface(gp_Ax3(center.toPnt(), dir.toDir()),
angle * DEG2RAD, # TODO why no orientation?
radius)
#2. construct an semgent in the u,v domain
geom_line = Geom2d_Line(gp_Pnt2d(0.0,0.0), gp_Dir2d(2*pi,pitch))
# 2. construct an semgent in the u,v domain
geom_line = Geom2d_Line(gp_Pnt2d(0.0, 0.0), gp_Dir2d(2 * pi, pitch))
#3. put it together into a wire
n_turns = height/pitch
# 3. put it together into a wire
n_turns = height / pitch
u_start = geom_line.Value(0.)
u_stop = geom_line.Value(sqrt(n_turns*((2*pi)**2 + pitch**2)))
u_stop = geom_line.Value(sqrt(n_turns * ((2 * pi)**2 + pitch**2)))
geom_seg = GCE2d_MakeSegment(u_start, u_stop).Value()
e = BRepBuilderAPI_MakeEdge(geom_seg, geom_surf.GetHandle()).Edge()
#4. Convert to wire and fix building 3d geom from 2d geom
# 4. Convert to wire and fix building 3d geom from 2d geom
w = BRepBuilderAPI_MakeWire(e).Wire()
breplib_BuildCurves3d(w)
return cls(w)
def stitch(self,other):
def stitch(self, other):
"""Attempt to stich wires"""
wire_builder = BRepBuilderAPI_MakeWire()
@ -861,6 +868,7 @@ class Wire(Shape, Mixin1D):
return self.__class__(wire_builder.Wire())
class Face(Shape):
"""
a bounded surface that represents part of the boundary of a solid
@ -870,7 +878,7 @@ class Face(Shape):
"""
Return the underlying geometry
"""
return BRep_Tool.Surface(self.wrapped) #BRepAdaptor_Surface(self.wrapped)
return BRep_Tool.Surface(self.wrapped) # BRepAdaptor_Surface(self.wrapped)
def _uvBounds(self):
@ -884,24 +892,23 @@ class Face(Shape):
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
:type locationVector: a vector that lies on the surface.
"""
#get the geometry
# get the geometry
surface = self._geomAdaptor()
if locationVector is None:
u0, u1, v0, v1 = self._uvBounds()
u = 0.5*(u0 + u1)
v = 0.5*(v0 + v1)
u = 0.5 * (u0 + u1)
v = 0.5 * (v0 + v1)
else:
#project point on surface
# project point on surface
projector = GeomAPI_ProjectPointOnSurf(locationVector.toPnt(),
surface)
u,v = projector.LowerDistanceParameters()
u, v = projector.LowerDistanceParameters()
p = gp_Pnt()
vn = gp_Vec()
BRepGProp_Face(self.wrapped).Normal(u,v,p,vn)
BRepGProp_Face(self.wrapped).Normal(u, v, p, vn)
return Vector(vn)
@ -918,13 +925,13 @@ class Face(Shape):
basePnt = Vector(basePnt)
dir = Vector(dir)
pln_geom = gp_Pln(basePnt.toPnt(),dir.toDir())
pln_geom = gp_Pln(basePnt.toPnt(), dir.toDir())
return cls(BRepBuilderAPI_MakeFace(pln_geom,
-width*0.5,
width*0.5,
-length*0.5,
length*0.5).Face())
-width * 0.5,
width * 0.5,
-length * 0.5,
length * 0.5).Face())
@classmethod
def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None):
@ -934,7 +941,7 @@ class Face(Shape):
these must have the same number of edges
"""
if isinstance(edgeOrWire1,Wire):
if isinstance(edgeOrWire1, Wire):
return cls.cast(brepfill_Shell(edgeOrWire1.wrapped,
edgeOrWire1.wrapped))
else:
@ -947,18 +954,19 @@ class Face(Shape):
Makes a planar face from one or more wires
'''
face_builder = BRepBuilderAPI_MakeFace(outerWire.wrapped,
True) #True is for planar only
True) # True is for planar only
for w in innerWires:
face_builder.Add(w.wrapped)
face_builder.Build()
f = face_builder.Face()
sf = ShapeFix_Face(f) #fix wire orientation
sf = ShapeFix_Face(f) # fix wire orientation
sf.FixOrientation()
return cls(sf.Face())
class Shell(Shape):
"""
the outer boundary of a surface
@ -994,7 +1002,7 @@ class Mixin3D(object):
fillet_builder = BRepFilletAPI_MakeFillet(self.wrapped)
for e in nativeEdges:
fillet_builder.Add(radius,e)
fillet_builder.Add(radius, e)
return self.__class__(fillet_builder.Shape())
@ -1008,7 +1016,7 @@ class Mixin3D(object):
"""
nativeEdges = [e.wrapped for e in edgeList]
#make a edge --> faces mapping
# make a edge --> faces mapping
edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape()
topexp_MapShapesAndAncestors(self.wrapped,
@ -1031,7 +1039,7 @@ class Mixin3D(object):
chamfer_builder.Add(d1,
d2,
e,
topods_Face(face)) #NB: edge_face_map return a generic TopoDS_Shape
topods_Face(face)) # NB: edge_face_map return a generic TopoDS_Shape
return self.__class__(chamfer_builder.Shape())
def shell(self, faceList, thickness, tolerance=0.0001):
@ -1057,7 +1065,8 @@ class Mixin3D(object):
return self.__class__(shell_builder.Shape())
class Solid(Shape,Mixin3D):
class Solid(Shape, Mixin3D):
"""
a single solid
"""
@ -1097,7 +1106,7 @@ class Solid(Shape,Mixin3D):
radius1,
radius2,
height,
angleDegrees*DEG2RAD).Shape())
angleDegrees * DEG2RAD).Shape())
@classmethod
def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360):
@ -1110,7 +1119,7 @@ class Solid(Shape,Mixin3D):
dir.toDir()),
radius,
height,
angleDegrees*DEG2RAD).Shape())
angleDegrees * DEG2RAD).Shape())
@classmethod
def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None):
@ -1124,8 +1133,8 @@ class Solid(Shape,Mixin3D):
dir.toDir()),
radius1,
radius2,
angleDegrees1*DEG2RAD,
angleDegrees2*DEG2RAD).Shape())
angleDegrees1 * DEG2RAD,
angleDegrees2 * DEG2RAD).Shape())
@classmethod
def makeLoft(cls, listOfWire, ruled=False):
@ -1145,7 +1154,7 @@ class Solid(Shape,Mixin3D):
return cls(loft_builder.Shape())
@classmethod
def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=Vector(0,0,0), dir=Vector(0,0,1)):
def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)):
"""
Make a wedge located in pnt
By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)
@ -1164,7 +1173,7 @@ class Solid(Shape,Mixin3D):
x2max).Solid())
@classmethod
def makeSphere(cls, radius, pnt=Vector(0,0,0), dir=Vector(0,0,1), angleDegrees1=0, angleDegrees2=90, angleDegrees3=360):
def makeSphere(cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees1=0, angleDegrees2=90, angleDegrees3=360):
"""
Make a sphere with a given radius
By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360
@ -1172,17 +1181,17 @@ class Solid(Shape,Mixin3D):
return cls(BRepPrimAPI_MakeSphere(gp_Ax2(pnt.toPnt(),
dir.toDir()),
radius,
angleDegrees1*DEG2RAD,
angleDegrees2*DEG2RAD,
angleDegrees3*DEG2RAD).Shape())
angleDegrees1 * DEG2RAD,
angleDegrees2 * DEG2RAD,
angleDegrees3 * DEG2RAD).Shape())
@classmethod
def _extrudeAuxSpine(cls,wire,spine,auxSpine):
def _extrudeAuxSpine(cls, wire, spine, auxSpine):
"""
Helper function for extrudeLinearWithRotation
"""
extrude_builder = BRepOffsetAPI_MakePipeShell(spine)
extrude_builder.SetMode(auxSpine,False) #auxiliary spine
extrude_builder.SetMode(auxSpine, False) # auxiliary spine
extrude_builder.Add(wire)
extrude_builder.Build()
extrude_builder.MakeSolid()
@ -1211,11 +1220,11 @@ class Solid(Shape,Mixin3D):
:return: a cad.Solid object
"""
# make straight spine
straight_spine_e = Edge.makeLine(vecCenter,vecCenter.add(vecNormal))
straight_spine_w = Wire.combine([straight_spine_e,]).wrapped
straight_spine_e = Edge.makeLine(vecCenter, vecCenter.add(vecNormal))
straight_spine_w = Wire.combine([straight_spine_e, ]).wrapped
# make an auxliliary spine
pitch = 360./angleDegrees * vecNormal.Length
pitch = 360. / angleDegrees * vecNormal.Length
radius = 1
aux_spine_w = Wire.makeHelix(pitch,
vecNormal.Length,
@ -1236,13 +1245,13 @@ class Solid(Shape,Mixin3D):
# combine dthe inner solids into compund
inner_comp = TopoDS_Compound()
comp_builder = TopoDS_Builder()
comp_builder.MakeCompound(inner_comp) #TODO this could be not needed
comp_builder.MakeCompound(inner_comp) # TODO this could be not needed
for i in inner_solids: comp_builder.Add(inner_comp,i)
for i in inner_solids:
comp_builder.Add(inner_comp, i)
# subtract from the outer solid
return cls(BRepAlgoAPI_Cut(outer_solid,inner_comp).Shape())
return cls(BRepAlgoAPI_Cut(outer_solid, inner_comp).Shape())
@classmethod
def extrudeLinear(cls, outerWire, innerWires, vecNormal):
@ -1271,12 +1280,13 @@ class Solid(Shape,Mixin3D):
# one would think that fusing faces into a compound and then extruding would work,
# but it doesnt-- the resulting compound appears to look right, ( right number of faces, etc),
# but then cutting it from the main solid fails with BRep_NotDone.
#the work around is to extrude each and then join the resulting solids, which seems to work
# 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
face = Face.makeFromWires(outerWire, innerWires)
prism_builder = BRepPrimAPI_MakePrism(face.wrapped, vecNormal.wrapped, True)
prism_builder = BRepPrimAPI_MakePrism(
face.wrapped, vecNormal.wrapped, True)
return cls(prism_builder.Shape())
@ -1312,8 +1322,8 @@ class Solid(Shape,Mixin3D):
v2 = Vector(axisEnd)
v2 = v2 - v1
revol_builder = BRepPrimAPI_MakeRevol(face.wrapped,
gp_Ax1(v1.toPnt(),v2.toDir()),
angleDegrees*DEG2RAD,
gp_Ax1(v1.toPnt(), v2.toDir()),
angleDegrees * DEG2RAD,
True)
return cls(revol_builder.Shape())
@ -1330,12 +1340,12 @@ class Solid(Shape,Mixin3D):
"""
if path.ShapeType() == 'Edge':
path = Wire.assembleEdges([path,])
path = Wire.assembleEdges([path, ])
if makeSolid:
face = Face.makeFromWires(outerWire, innerWires)
builder = BRepOffsetAPI_MakePipe(path.wrapped,face.wrapped)
builder = BRepOffsetAPI_MakePipe(path.wrapped, face.wrapped)
else:
builder = BRepOffsetAPI_MakePipeShell(path.wrapped)
@ -1347,7 +1357,8 @@ class Solid(Shape,Mixin3D):
return cls(builder.Shape())
class Compound(Shape,Mixin3D):
class Compound(Shape, Mixin3D):
"""
a collection of disconnected solids
"""
@ -1359,13 +1370,16 @@ class Compound(Shape,Mixin3D):
"""
comp = TopoDS_Compound()
comp_builder = TopoDS_Builder()
comp_builder.MakeCompound(comp) #TODO this could be not needed
comp_builder.MakeCompound(comp) # TODO this could be not needed
for s in listOfShapes: comp_builder.Add(comp,s.wrapped)
for s in listOfShapes:
comp_builder.Add(comp, s.wrapped)
return cls(comp)
# TODO this is likely not needed if sing PythonOCC correclty but we will see
def sortWiresByBuildOrder(wireList, plane, result=[]):
"""Tries to determine how wires should be combined into faces.
@ -1383,26 +1397,27 @@ def sortWiresByBuildOrder(wireList, plane, result=[]):
Returns, list of lists.
"""
#check if we have something to sort at all
if len(wireList)<2:
return [wireList,]
# check if we have something to sort at all
if len(wireList) < 2:
return [wireList, ]
#make a Face
face = Face.makeFromWires(wireList[0],wireList[1:])
# make a Face
face = Face.makeFromWires(wireList[0], wireList[1:])
#use FixOrientation
# use FixOrientation
outer_inner_map = TopTools_DataMapOfShapeListOfShape()
sf = ShapeFix_Face(face.wrapped) #fix wire orientation
sf = ShapeFix_Face(face.wrapped) # fix wire orientation
sf.FixOrientation(outer_inner_map)
#Iterate through the Inner:Outer Mapping
# Iterate through the Inner:Outer Mapping
all_wires = face.Wires()
result = {w:outer_inner_map.Find(w.wrapped) for w in all_wires if outer_inner_map.IsBound(w.wrapped)}
result = {w: outer_inner_map.Find(
w.wrapped) for w in all_wires if outer_inner_map.IsBound(w.wrapped)}
#construct the result
# construct the result
rv = []
for k,v in result.iteritems():
tmp = [k,]
for k, v in result.items():
tmp = [k, ]
iterator = TopTools_ListIteratorOfListOfShape(v)
while iterator.More():

View File

@ -19,11 +19,12 @@
import re
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 pyparsing import Literal,Word,nums,Optional,Combine,oneOf,upcaseTokens,\
CaselessLiteral,Group,infixNotation,opAssoc,Forward,\
ZeroOrMore,Keyword
from pyparsing import Literal, Word, nums, Optional, Combine, oneOf, upcaseTokens,\
CaselessLiteral, Group, infixNotation, opAssoc, Forward,\
ZeroOrMore, Keyword
from functools import reduce
class Selector(object):
@ -32,7 +33,8 @@ class Selector(object):
Filters must provide a single method that filters objects.
"""
def filter(self,objectList):
def filter(self, objectList):
"""
Filter the provided list
:param objectList: list to filter
@ -56,6 +58,7 @@ class Selector(object):
def __neg__(self):
return InverseSelector(self)
class NearestToPointSelector(Selector):
"""
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
"""
def __init__(self,pnt ):
def __init__(self, pnt):
self.pnt = pnt
def filter(self,objectList):
def filter(self, objectList):
def dist(tShape):
return tShape.Center().sub(Vector(*self.pnt)).Length
#if tShape.ShapeType == 'Vertex':
# if tShape.ShapeType == 'Vertex':
# return tShape.Point.sub(toVector(self.pnt)).Length
#else:
# else:
# return tShape.CenterOfMass.sub(toVector(self.pnt)).Length
return [ min(objectList,key=dist) ]
return [min(objectList, key=dist)]
class BoxSelector(Selector):
"""
@ -100,6 +106,7 @@ class BoxSelector(Selector):
CQ(aCube).edges(BoxSelector((0,1,0), (1,2,1))
"""
def __init__(self, point0, point1, boundingbox=False):
self.p0 = Vector(*point0)
self.p1 = Vector(*point1)
@ -130,20 +137,22 @@ class BoxSelector(Selector):
return result
class BaseDirSelector(Selector):
"""
A selector that handles selection on the basis of a single
direction vector
"""
def __init__(self,vector,tolerance=0.0001 ):
def __init__(self, vector, tolerance=0.0001):
self.direction = vector
self.TOLERANCE = tolerance
def test(self,vec):
def test(self, vec):
"Test a specified vector. Subclasses override to provide other implementations"
return True
def filter(self,objectList):
def filter(self, objectList):
"""
There are lots of kinds of filters, but
for planes they are always based on the normal of the plane,
@ -151,7 +160,7 @@ class BaseDirSelector(Selector):
"""
r = []
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:
# 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):
r.append(o)
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)
if self.test(tangent):
r.append(o)
return r
class ParallelDirSelector(BaseDirSelector):
"""
Selects objects parallel with the provided direction
@ -187,9 +197,10 @@ class ParallelDirSelector(BaseDirSelector):
CQ(aCube).faces("|Z")
"""
def test(self,vec):
def test(self, vec):
return self.direction.cross(vec).Length < self.TOLERANCE
class DirectionSelector(BaseDirSelector):
"""
Selects objects aligned with the provided direction
@ -210,9 +221,10 @@ class DirectionSelector(BaseDirSelector):
CQ(aCube).faces("+Z")
"""
def test(self,vec):
def test(self, vec):
return abs(self.direction.getAngle(vec) < self.TOLERANCE)
class PerpendicularDirSelector(BaseDirSelector):
"""
Selects objects perpendicular with the provided direction
@ -233,9 +245,10 @@ class PerpendicularDirSelector(BaseDirSelector):
CQ(aCube).faces("#Z")
"""
def test(self,vec):
def test(self, 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
@ -259,16 +272,18 @@ class TypeSelector(Selector):
CQ(aCube).faces( "%PLANE" )
"""
def __init__(self,typeString):
def __init__(self, typeString):
self.typeString = typeString.upper()
def filter(self,objectList):
def filter(self, objectList):
r = []
for o in objectList:
if o.geomType() == self.typeString:
r.append(o)
return r
class DirectionMinMaxSelector(Selector):
"""
Selects objects closest or farthest in the specified direction
@ -291,32 +306,35 @@ class DirectionMinMaxSelector(Selector):
CQ(aCube).faces( ">Z" )
"""
def __init__(self, vector, directionMax=True, tolerance=0.0001):
self.vector = vector
self.max = max
self.directionMax = directionMax
self.TOLERANCE = tolerance
def filter(self,objectList):
def filter(self, objectList):
def distance(tShape):
return tShape.Center().dot(self.vector)
# import OrderedDict
from collections import OrderedDict
#make and distance to object dict
objectDict = {distance(el) : el for el in objectList}
#transform it into an ordered dict
objectDict = OrderedDict(sorted(objectDict.items(),
# make and distance to object dict
objectDict = {distance(el): el for el in objectList}
# transform it into an ordered dict
objectDict = OrderedDict(sorted(list(objectDict.items()),
key=lambda x: x[0]))
# find out the max/min distance
if self.directionMax:
d = objectDict.keys()[-1]
d = list(objectDict.keys())[-1]
else:
d = objectDict.keys()[0]
d = list(objectDict.keys())[0]
# 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):
"""
@ -327,6 +345,7 @@ class DirectionNthSelector(ParallelDirSelector):
Linear Edges
Planar Faces
"""
def __init__(self, vector, n, directionMax=True, tolerance=0.0001):
self.direction = vector
self.max = max
@ -334,34 +353,36 @@ class DirectionNthSelector(ParallelDirSelector):
self.TOLERANCE = tolerance
self.N = n
def filter(self,objectList):
#select first the objects that are normal/parallel to a given dir
objectList = super(DirectionNthSelector,self).filter(objectList)
def filter(self, objectList):
# select first the objects that are normal/parallel to a given dir
objectList = super(DirectionNthSelector, self).filter(objectList)
def distance(tShape):
return tShape.Center().dot(self.direction)
#calculate how many digits of precision do we need
digits = int(1/self.TOLERANCE)
# calculate how many digits of precision do we need
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
# make a distance to object dict
# this is one to many mapping so I am using a default dict with list
objectDict = defaultdict(list)
for el in objectList:
objectDict[round(distance(el),digits)].append(el)
objectDict[round(distance(el), digits)].append(el)
# choose the Nth unique rounded distance
nth_distance = sorted(objectDict.keys(),
nth_distance = sorted(list(objectDict.keys()),
reverse=not self.directionMax)[self.N]
# map back to original objects and return
return objectDict[nth_distance]
class BinarySelector(Selector):
"""
Base class for selectors that operates with two other
selectors. Subclass must implement the :filterResults(): method.
"""
def __init__(self, left, right):
self.left = left
self.right = right
@ -373,35 +394,43 @@ class BinarySelector(Selector):
def filterResults(self, r_left, r_right):
raise NotImplementedError
class AndSelector(BinarySelector):
"""
Intersection selector. Returns objects that is selected by both selectors.
"""
def filterResults(self, r_left, r_right):
# return intersection of lists
return list(set(r_left) & set(r_right))
class SumSelector(BinarySelector):
"""
Union selector. Returns the sum of two selectors results.
"""
def filterResults(self, r_left, r_right):
# return the union (no duplicates) of lists
return list(set(r_left + r_right))
class SubtractSelector(BinarySelector):
"""
Difference selector. Substract results of a selector from another
selectors results.
"""
def filterResults(self, r_left, r_right):
return list(set(r_left) - set(r_right))
class InverseSelector(Selector):
"""
Inverts the selection of given selector. In other words, selects
all objects that is not selected by given selector.
"""
def __init__(self, selector):
self.selector = selector
@ -415,47 +444,47 @@ def _makeGrammar():
Define the simple string selector grammar using PyParsing
"""
#float definition
# float definition
point = Literal('.')
plusmin = Literal('+') | Literal('-')
number = Word(nums)
integer = Combine(Optional(plusmin) + number)
floatn = Combine(integer + Optional(point + Optional(number)))
#vector definition
# vector definition
lbracket = Literal('(')
rbracket = Literal(')')
comma = Literal(',')
vector = Combine(lbracket + floatn('x') + comma + \
vector = Combine(lbracket + floatn('x') + comma +
floatn('y') + comma + floatn('z') + rbracket)
#direction definition
simple_dir = oneOf(['X','Y','Z','XY','XZ','YZ'])
# direction definition
simple_dir = oneOf(['X', 'Y', 'Z', 'XY', 'XZ', 'YZ'])
direction = simple_dir('simple_dir') | vector('vector_dir')
#CQ type definition
cqtype = oneOf(['Plane','Cylinder','Sphere','Cone','Line','Circle','Arc'],
# CQ type definition
cqtype = oneOf(['Plane', 'Cylinder', 'Sphere', 'Cone', 'Line', 'Circle', 'Arc'],
caseless=True)
cqtype = cqtype.setParseAction(upcaseTokens)
#type operator
# type operator
type_op = Literal('%')
#direction operator
direction_op = oneOf(['>','<'])
# direction operator
direction_op = oneOf(['>', '<'])
#index definition
ix_number = Group(Optional('-')+Word(nums))
# index definition
ix_number = Group(Optional('-') + Word(nums))
lsqbracket = Literal('[').suppress()
rsqbracket = Literal(']').suppress()
index = lsqbracket + ix_number('index') + rsqbracket
#other operators
other_op = oneOf(['|','#','+','-'])
# other operators
other_op = oneOf(['|', '#', '+', '-'])
#named view
named_view = oneOf(['front','back','left','right','top','bottom'])
# named view
named_view = oneOf(['front', 'back', 'left', 'right', 'top', 'bottom'])
return direction('only_dir') | \
(type_op('type_op') + cqtype('cq_type')) | \
@ -463,51 +492,54 @@ def _makeGrammar():
(other_op('other_op') + direction('dir')) | \
named_view('named_view')
_grammar = _makeGrammar() #make a grammar instance
_grammar = _makeGrammar() # make a grammar instance
class _SimpleStringSyntaxSelector(Selector):
"""
This is a private class that converts a parseResults object into a simple
selector object
"""
def __init__(self,parseResults):
#define all token to object mappings
def __init__(self, parseResults):
# define all token to object mappings
self.axes = {
'X': Vector(1,0,0),
'Y': Vector(0,1,0),
'Z': Vector(0,0,1),
'XY': Vector(1,1,0),
'YZ': Vector(0,1,1),
'XZ': Vector(1,0,1)
'X': Vector(1, 0, 0),
'Y': Vector(0, 1, 0),
'Z': Vector(0, 0, 1),
'XY': Vector(1, 1, 0),
'YZ': Vector(0, 1, 1),
'XZ': Vector(1, 0, 1)
}
self.namedViews = {
'front' : (Vector(0,0,1),True),
'back' : (Vector(0,0,1),False),
'left' : (Vector(1,0,0),False),
'right' : (Vector(1,0,0),True),
'top' : (Vector(0,1,0),True),
'bottom': (Vector(0,1,0),False)
'front': (Vector(0, 0, 1), True),
'back': (Vector(0, 0, 1), False),
'left': (Vector(1, 0, 0), False),
'right': (Vector(1, 0, 0), True),
'top': (Vector(0, 1, 0), True),
'bottom': (Vector(0, 1, 0), False)
}
self.operatorMinMax = {
'>' : True,
'<' : False,
'+' : True,
'-' : False
'>': True,
'<': False,
'+': True,
'-': False
}
self.operator = {
'+' : DirectionSelector,
'-' : DirectionSelector,
'#' : PerpendicularDirSelector,
'|' : ParallelDirSelector}
'+': DirectionSelector,
'-': DirectionSelector,
'#': PerpendicularDirSelector,
'|': ParallelDirSelector}
self.parseResults = parseResults
self.mySelector = self._chooseSelector(parseResults)
def _chooseSelector(self,pr):
def _chooseSelector(self, pr):
"""
Sets up the underlying filters accordingly
"""
@ -523,9 +555,9 @@ class _SimpleStringSyntaxSelector(Selector):
minmax = self.operatorMinMax[pr.dir_op]
if 'index' in pr:
return DirectionNthSelector(vec,int(''.join(pr.index.asList())),minmax)
return DirectionNthSelector(vec, int(''.join(pr.index.asList())), minmax)
else:
return DirectionMinMaxSelector(vec,minmax)
return DirectionMinMaxSelector(vec, minmax)
elif 'other_op' in pr:
vec = self._getVector(pr)
@ -535,68 +567,75 @@ class _SimpleStringSyntaxSelector(Selector):
args = self.namedViews[pr.named_view]
return DirectionMinMaxSelector(*args)
def _getVector(self,pr):
def _getVector(self, pr):
"""
Translate parsed vector string into a CQ Vector
"""
if 'vector_dir' in pr:
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:
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
[+\|-\|<\|>\|] \<X\|Y\|Z>
"""
return self.mySelector.filter(objectList)
def _makeExpressionGrammar(atom):
"""
Define the complex string selector grammar using PyParsing (which supports
logical operations and nesting)
"""
#define operators
# define operators
and_op = Literal('and')
or_op = Literal('or')
delta_op = oneOf(['exc','except'])
delta_op = oneOf(['exc', 'except'])
not_op = Literal('not')
def atom_callback(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):
items = res.asList()[0][::2] #take every secend items, i.e. all operands
return reduce(AndSelector,items)
# take every secend items, i.e. all operands
items = res.asList()[0][::2]
return reduce(AndSelector, items)
def or_callback(res):
items = res.asList()[0][::2] #take every secend items, i.e. all operands
return reduce(SumSelector,items)
# take every secend items, i.e. all operands
items = res.asList()[0][::2]
return reduce(SumSelector, items)
def exc_callback(res):
items = res.asList()[0][::2] #take every secend items, i.e. all operands
return reduce(SubtractSelector,items)
# take every secend items, i.e. all operands
items = res.asList()[0][::2]
return reduce(SubtractSelector, items)
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)
#construct the final grammar and set all the callbacks
# construct the final grammar and set all the callbacks
expr = infixNotation(atom,
[(and_op,2,opAssoc.LEFT,and_callback),
(or_op,2,opAssoc.LEFT,or_callback),
(delta_op,2,opAssoc.LEFT,exc_callback),
(not_op,1,opAssoc.RIGHT,not_callback)])
[(and_op, 2, opAssoc.LEFT, and_callback),
(or_op, 2, opAssoc.LEFT, or_callback),
(delta_op, 2, opAssoc.LEFT, exc_callback),
(not_op, 1, opAssoc.RIGHT, not_callback)])
return expr
_expression_grammar = _makeExpressionGrammar(_grammar)
class StringSyntaxSelector(Selector):
"""
Filter lists objects using a simple string syntax. All of the filters available in the string syntax
@ -647,7 +686,8 @@ class StringSyntaxSelector(Selector):
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
"""
@ -656,7 +696,7 @@ class StringSyntaxSelector(Selector):
parseAll=True)
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
"""

View File

@ -36,22 +36,24 @@ TEST_DEBUG_SCRIPT = textwrap.dedent(
"""
)
class TestCQGI(BaseTest):
def test_parser(self):
model = cqgi.CQModel(TESTSCRIPT)
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):
model = cqgi.CQModel(TEST_DEBUG_SCRIPT)
result = model.build()
debugItems = result.debugObjects
self.assertTrue(len(debugItems) == 2)
self.assertTrue( debugItems[0].object == "bar" )
self.assertTrue( debugItems[0].args == { "color":'yellow' } )
self.assertTrue( debugItems[1].object == 2.0 )
self.assertTrue( debugItems[1].args == {} )
self.assertTrue(debugItems[0].object == "bar")
self.assertTrue(debugItems[0].args == {"color": 'yellow'})
self.assertTrue(debugItems[1].object == 2.0)
self.assertTrue(debugItems[1].args == {})
def test_build_with_empty_params(self):
model = cqgi.CQModel(TESTSCRIPT)
@ -77,7 +79,7 @@ class TestCQGI(BaseTest):
a_param = model.metadata.parameters['a']
self.assertTrue(a_param.default_value == 2.0)
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):
script = textwrap.dedent(
@ -88,7 +90,7 @@ class TestCQGI(BaseTest):
)
model = cqgi.CQModel(script)
a_param = model.metadata.parameters['a']
self.assertTrue(a_param.name == 'a' )
self.assertTrue(a_param.name == 'a')
def test_build_with_exception(self):
badscript = textwrap.dedent(
@ -127,9 +129,9 @@ class TestCQGI(BaseTest):
model = cqgi.CQModel(script)
result = model.build({})
self.assertEquals(2, len(result.results))
self.assertEquals(1, result.results[0])
self.assertEquals(2, result.results[1])
self.assertEqual(2, len(result.results))
self.assertEqual(1, result.results[0])
self.assertEqual(2, result.results[1])
def test_that_assinging_number_to_string_works(self):
script = textwrap.dedent(
@ -138,8 +140,8 @@ class TestCQGI(BaseTest):
build_object(h)
"""
)
result = cqgi.parse(script).build( {'h': 33.33})
self.assertEquals(result.results[0], "33.33")
result = cqgi.parse(script).build({'h': 33.33})
self.assertEqual(result.results[0], "33.33")
def test_that_assigning_string_to_number_fails(self):
script = textwrap.dedent(
@ -148,8 +150,9 @@ class TestCQGI(BaseTest):
build_object(h)
"""
)
result = cqgi.parse(script).build( {'h': "a string"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
result = cqgi.parse(script).build({'h': "a string"})
self.assertTrue(isinstance(result.exception,
cqgi.InvalidParameterError))
def test_that_assigning_unknown_var_fails(self):
script = textwrap.dedent(
@ -159,8 +162,9 @@ class TestCQGI(BaseTest):
"""
)
result = cqgi.parse(script).build( {'w': "var is not there"})
self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError))
result = cqgi.parse(script).build({'w': "var is not there"})
self.assertTrue(isinstance(result.exception,
cqgi.InvalidParameterError))
def test_that_not_calling_build_object_raises_error(self):
script = textwrap.dedent(
@ -195,7 +199,7 @@ class TestCQGI(BaseTest):
result = cqgi.parse(script).build({'h': False})
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):
script = textwrap.dedent(
@ -213,4 +217,4 @@ class TestCQGI(BaseTest):
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 unittest,sys
import unittest
import sys
import os.path
#my modules
from tests import BaseTest,makeUnitCube,makeUnitSquareWire
# my modules
from tests import BaseTest, makeUnitCube, makeUnitSquareWire
from cadquery import *
from cadquery import selectors
class TestCQSelectors(BaseTest):
class TestCQSelectors(BaseTest):
def testWorkplaneCenter(self):
"Test Moving workplane center"
s = Workplane(Plane.XY())
#current point and world point should be equal
self.assertTupleAlmostEquals((0.0,0.0,0.0),s.plane.origin.toTuple(),3)
# current point and world point should be equal
self.assertTupleAlmostEquals(
(0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3)
#move origin and confirm center moves
s.center(-2.0,-2.0)
# move origin and confirm center moves
s.center(-2.0, -2.0)
#current point should be 0,0, but
self.assertTupleAlmostEquals((-2.0,-2.0,0.0),s.plane.origin.toTuple(),3)
# current point should be 0,0, but
self.assertTupleAlmostEquals(
(-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3)
def testVertices(self):
t = makeUnitSquareWire() # square box
c = CQ(t)
self.assertEqual(4,c.vertices().size() )
self.assertEqual(4,c.edges().size() )
self.assertEqual(0,c.vertices().edges().size() ) #no edges on any vertices
self.assertEqual(4,c.edges().vertices().size() ) #but selecting all edges still yields all vertices
self.assertEqual(1,c.wires().size()) #just one wire
self.assertEqual(0,c.faces().size())
self.assertEqual(0,c.vertices().faces().size()) #odd combinations all work but yield no results
self.assertEqual(0,c.edges().faces().size())
self.assertEqual(0,c.edges().vertices().faces().size())
self.assertEqual(4, c.vertices().size())
self.assertEqual(4, c.edges().size())
self.assertEqual(0, c.vertices().edges().size()
) # no edges on any vertices
# but selecting all edges still yields all vertices
self.assertEqual(4, c.edges().vertices().size())
self.assertEqual(1, c.wires().size()) # just one wire
self.assertEqual(0, c.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):
c = CQ(makeUnitSquareWire())
self.assertEqual(4,c.vertices().size() ) #4 because there are 4 vertices
self.assertEqual(1,c.vertices().end().size() ) #1 because we started with 1 wire
# 4 because there are 4 vertices
self.assertEqual(4, c.vertices().size())
# 1 because we started with 1 wire
self.assertEqual(1, c.vertices().end().size())
def testAll(self):
"all returns a list of CQ objects, so that you can iterate over them individually"
c = CQ(makeUnitCube())
self.assertEqual(6,c.faces().size())
self.assertEqual(6,len(c.faces().all()))
self.assertEqual(4,c.faces().all()[0].vertices().size() )
self.assertEqual(6, c.faces().size())
self.assertEqual(6, len(c.faces().all()))
self.assertEqual(4, c.faces().all()[0].vertices().size())
def testFirst(self):
c = CQ( makeUnitCube())
self.assertEqual(type(c.vertices().first().val()),Vertex)
self.assertEqual(type(c.vertices().first().first().first().val()),Vertex)
c = CQ(makeUnitCube())
self.assertEqual(type(c.vertices().first().val()), Vertex)
self.assertEqual(
type(c.vertices().first().first().first().val()), Vertex)
def testCompounds(self):
c = CQ(makeUnitSquareWire())
self.assertEqual(0,c.compounds().size() )
self.assertEqual(0,c.shells().size() )
self.assertEqual(0,c.solids().size() )
self.assertEqual(0, c.compounds().size())
self.assertEqual(0, c.shells().size())
self.assertEqual(0, c.solids().size())
def testSolid(self):
c = CQ(makeUnitCube())
#make sure all the counts are right for a cube
self.assertEqual(1,c.solids().size() )
self.assertEqual(6,c.faces().size() )
self.assertEqual(12,c.edges().size())
self.assertEqual(8,c.vertices().size() )
self.assertEqual(0,c.compounds().size())
#now any particular face should result in 4 edges and four vertices
self.assertEqual(4,c.faces().first().edges().size() )
self.assertEqual(1,c.faces().first().size() )
self.assertEqual(4,c.faces().first().vertices().size() )
self.assertEqual(4,c.faces().last().edges().size() )
# make sure all the counts are right for a cube
self.assertEqual(1, c.solids().size())
self.assertEqual(6, c.faces().size())
self.assertEqual(12, c.edges().size())
self.assertEqual(8, c.vertices().size())
self.assertEqual(0, c.compounds().size())
# now any particular face should result in 4 edges and four vertices
self.assertEqual(4, c.faces().first().edges().size())
self.assertEqual(1, c.faces().first().size())
self.assertEqual(4, c.faces().first().vertices().size())
self.assertEqual(4, c.faces().last().edges().size())
def testFaceTypesFilter(self):
"Filters by face type"
@ -102,16 +108,16 @@ class TestCQSelectors(BaseTest):
def testPerpendicularDirFilter(self):
c = CQ(makeUnitCube())
self.assertEqual(8,c.edges("#Z").size() ) #8 edges are perp. to z
self.assertEqual(4, c.faces("#Z").size()) #4 faces are perp to z too!
self.assertEqual(8, c.edges("#Z").size()) # 8 edges are perp. to z
self.assertEqual(4, c.faces("#Z").size()) # 4 faces are perp to z too!
def testFaceDirFilter(self):
c = CQ(makeUnitCube())
#a cube has one face in each direction
# a cube has one face in each direction
self.assertEqual(1, c.faces("+Z").size())
self.assertEqual(1, c.faces("-Z").size())
self.assertEqual(1, c.faces("+X").size())
self.assertEqual(1, c.faces("X").size()) #should be same as +X
self.assertEqual(1, c.faces("X").size()) # should be same as +X
self.assertEqual(1, c.faces("-X").size())
self.assertEqual(1, c.faces("+Y").size())
self.assertEqual(1, c.faces("-Y").size())
@ -120,13 +126,15 @@ class TestCQSelectors(BaseTest):
def testParallelPlaneFaceFilter(self):
c = CQ(makeUnitCube())
#faces parallel to Z axis
# faces parallel to Z axis
self.assertEqual(2, c.faces("|Z").size())
#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(Vector((0,0,-1)))).size()) #same thing as above
# 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(
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())
def testParallelEdgeFilter(self):
@ -138,14 +146,14 @@ class TestCQSelectors(BaseTest):
def testMaxDistance(self):
c = CQ(makeUnitCube())
#should select the topmost face
# should select the topmost face
self.assertEqual(1, c.faces(">Z").size())
self.assertEqual(4, c.faces(">Z").vertices().size())
#vertices should all be at z=1, if this is the top face
self.assertEqual(4, len(c.faces(">Z").vertices().vals() ))
# vertices should all be at z=1, if this is the top face
self.assertEqual(4, len(c.faces(">Z").vertices().vals()))
for v in c.faces(">Z").vertices().vals():
self.assertAlmostEqual(1.0,v.Z,3)
self.assertAlmostEqual(1.0, v.Z, 3)
# test the case of multiple objects at the same distance
el = c.edges("<Z").vals()
@ -154,101 +162,106 @@ class TestCQSelectors(BaseTest):
def testMinDistance(self):
c = CQ(makeUnitCube())
#should select the topmost face
# should select the topmost face
self.assertEqual(1, c.faces("<Z").size())
self.assertEqual(4, c.faces("<Z").vertices().size())
#vertices should all be at z=1, if this is the top face
self.assertEqual(4, len(c.faces("<Z").vertices().vals() ))
# vertices should all be at z=1, if this is the top face
self.assertEqual(4, len(c.faces("<Z").vertices().vals()))
for v in c.faces("<Z").vertices().vals():
self.assertAlmostEqual(0.0,v.Z,3)
self.assertAlmostEqual(0.0, v.Z, 3)
# test the case of multiple objects at the same distance
el = c.edges("<Z").vals()
self.assertEqual(4, len(el))
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
val = c.faces(selectors.DirectionNthSelector(Vector(1,0,0),1)).val()
self.assertAlmostEqual(val.Center().x,-1.5)
# 2nd face
val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val()
self.assertAlmostEqual(val.Center().x, -1.5)
#2nd face with inversed selection vector
val = c.faces(selectors.DirectionNthSelector(Vector(-1,0,0),1)).val()
self.assertAlmostEqual(val.Center().x,1.5)
# 2nd face with inversed selection vector
val = c.faces(selectors.DirectionNthSelector(
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()
self.assertAlmostEqual(val.Center().x,1.5)
# 2nd last face
val = c.faces(selectors.DirectionNthSelector(
Vector(1, 0, 0), -2)).val()
self.assertAlmostEqual(val.Center().x, 1.5)
#Last face
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(
Vector(1, 0, 0), -1)).val()
self.assertAlmostEqual(val.Center().x, 2.5)
#check if the selected face if normal to the specified Vector
self.assertAlmostEqual(val.normalAt().cross(Vector(1,0,0)).Length,0.0)
# check if the selected face if normal to the specified Vector
self.assertAlmostEqual(
val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0)
#repeat the test using string based selector
# repeat the test using string based selector
#2nd face
# 2nd face
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()
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()
self.assertAlmostEqual(val.Center().x,1.5)
self.assertAlmostEqual(val.Center().x, 1.5)
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()
self.assertAlmostEqual(val.Center().x,1.5)
self.assertAlmostEqual(val.Center().x, 1.5)
#Last face
# Last face
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
self.assertAlmostEqual(val.normalAt().cross(Vector(1,0,0)).Length,0.0)
# check if the selected face if normal to the specified Vector
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')\
.box(1,4,1,centered=(False,True,False)).faces('<Z')\
.box(2,2,2,centered=(True,True,False)).faces('>Z')\
.box(1,1,1,centered=(True,True,False))
.box(1, 4, 1, centered=(False, True, False)).faces('<Z')\
.box(2, 2, 2, centered=(True, True, False)).faces('>Z')\
.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()
self.assertEqual(len(vals),2)
self.assertEqual(len(vals), 2)
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()
self.assertEqual(len(vals),2)
self.assertEqual(len(vals), 2)
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()
self.assertEqual(len(vals),2)
self.assertEqual(len(vals), 2)
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()
val2 = c.faces('<Z').val()
self.assertTupleAlmostEquals(val1.Center().toTuple(),
val2.Center().toTuple(),
3)
#verify that >Z[-1] is equivalent to >Z
# verify that >Z[-1] is equivalent to >Z
val1 = c.faces('>Z[-1]').val()
val2 = c.faces('>Z').val()
self.assertTupleAlmostEquals(val1.Center().toTuple(),
@ -258,21 +271,21 @@ class TestCQSelectors(BaseTest):
def testNearestTo(self):
c = CQ(makeUnitCube())
#nearest vertex to origin is (0,0,0)
t = (0.1,0.1,0.1)
# nearest vertex to origin is (0,0,0)
t = (0.1, 0.1, 0.1)
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)
#nearest edge is the vertical side edge, 0,0,0 -> 0,0,1
t = (0.1, 0.1, 0.2)
# nearest edge is the vertical side edge, 0,0,0 -> 0,0,1
e = c.edges(selectors.NearestToPointSelector(t)).vals()[0]
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()
self.assertEqual(1,len(s))
self.assertEqual(1, len(s))
def testBox(self):
c = CQ(makeUnitCube())
@ -303,9 +316,11 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3)
# 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))
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))
# test edge selection
@ -330,9 +345,11 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3)
# 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))
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))
# test face selection
@ -357,17 +374,22 @@ class TestCQSelectors(BaseTest):
self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3)
# 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))
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))
# 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))
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))
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))
def testAndSelector(self):
@ -376,11 +398,12 @@ class TestCQSelectors(BaseTest):
S = selectors.StringSyntaxSelector
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))
# 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))
# test using extended string syntax
@ -453,24 +476,23 @@ class TestCQSelectors(BaseTest):
v = c.vertices('(>X and >Y) or (<X and <Y)').vals()
self.assertEqual(4, len(v))
def testFaceCount(self):
c = CQ(makeUnitCube())
self.assertEqual( 6, c.faces().size() )
self.assertEqual( 2, c.faces("|Z").size() )
self.assertEqual(6, c.faces().size())
self.assertEqual(2, c.faces("|Z").size())
def testVertexFilter(self):
"test selecting vertices on a face"
c = CQ(makeUnitCube())
#TODO: filters work ok, but they are in global coordinates which sux. it would be nice
#if they were available in coordinates local to the selected face
# TODO: filters work ok, but they are in global coordinates which sux. it would be nice
# if they were available in coordinates local to the selected face
v2 = c.faces("+Z").vertices("<XY")
self.assertEqual(1,v2.size() ) #another way
#make sure the vertex is the right one
self.assertEqual(1, v2.size()) # another way
# make sure the vertex is the right one
self.assertTupleAlmostEquals((0.0,0.0,1.0),v2.val().toTuple() ,3)
self.assertTupleAlmostEquals((0.0, 0.0, 1.0), v2.val().toTuple(), 3)
def testGrammar(self):
"""
@ -499,5 +521,5 @@ class TestCQSelectors(BaseTest):
'(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)',
'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 unittest
from tests import BaseTest
@ -11,11 +11,12 @@ from OCC.GC import GC_MakeCircle
from cadquery import *
class TestCadObjects(BaseTest):
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.)
return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge())
@ -33,17 +34,18 @@ class TestCadObjects(BaseTest):
"""
v = Vertex.makeVertex(1, 1, 1)
self.assertEqual(1, v.X)
self.assertEquals(Vector, type(v.Center()))
self.assertEqual(Vector, type(v.Center()))
def testBasicBoundingBox(self):
v = Vertex.makeVertex(1, 1, 1)
v2 = Vertex.makeVertex(2, 2, 2)
self.assertEquals(BoundBox, type(v.BoundingBox()))
self.assertEquals(BoundBox, type(v2.BoundingBox()))
self.assertEqual(BoundBox, type(v.BoundingBox()))
self.assertEqual(BoundBox, type(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):
e = self._make_circle()
@ -51,16 +53,20 @@ class TestCadObjects(BaseTest):
self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3)
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((10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3)
self.assertTupleAlmostEquals((-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3)
self.assertTupleAlmostEquals(
(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):
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):
pass
@ -72,6 +78,7 @@ class TestCadObjects(BaseTest):
"""
Tests whether or not a proper weighted center can be found for a compound
"""
def cylinders(self, radius, height):
def _cyl(pnt):
# Inner function to build a cylinder
@ -85,22 +92,24 @@ class TestCadObjects(BaseTest):
Workplane.cyl = cylinders
# 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.assertTupleAlmostEquals((0.0, 0.0, 0.25), s.val().Center().toTuple(), 3)
self.assertEqual(4, len(s.val().Solids()))
self.assertTupleAlmostEquals(
(0.0, 0.0, 0.25), s.val().Center().toTuple(), 3)
def testDot(self):
v1 = Vector(2, 2, 2)
v2 = Vector(1, -1, 1)
self.assertEquals(2.0, v1.dot(v2))
self.assertEqual(2.0, v1.dot(v2))
def testVectorAdd(self):
result = Vector(1, 2, 0) + Vector(0, 0, 3)
self.assertTupleAlmostEquals((1.0, 2.0, 3.0), result.toTuple(), 3)
def testTranslate(self):
e = Edge.makeCircle(2,(1,2,3))
e = Edge.makeCircle(2, (1, 2, 3))
e2 = e.translate(Vector(0, 0, 1))
self.assertTupleAlmostEquals((1.0, 2.0, 4.0), e2.Center().toTuple(), 3)
@ -108,7 +117,8 @@ class TestCadObjects(BaseTest):
def testVertices(self):
e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0),
gp_Pnt(1, 1, 0)).Edge())
self.assertEquals(2, len(e.Vertices()))
self.assertEqual(2, len(e.Vertices()))
if __name__ == '__main__':
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,49 @@
"""
Tests basic workplane functionality
"""
#core modules
import StringIO
# core modules
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 exporters
from tests import BaseTest
class TestExporters(BaseTest):
def _exportBox(self,eType,stringsToFind):
def _exportBox(self, eType, stringsToFind):
"""
Exports a test object, and then looks for
all of the supplied strings to be in the result
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()
exporters.exportShape(p,eType,s,0.1)
exporters.exportShape(p, eType, s, 0.1)
result = s.getvalue()
for q in stringsToFind:
self.assertTrue(result.find(q) > -1 )
self.assertTrue(result.find(q) > -1)
return result
def testSTL(self):
self._exportBox(exporters.ExportTypes.STL,['facet normal'])
self._exportBox(exporters.ExportTypes.STL, ['facet normal'])
def testSVG(self):
self._exportBox(exporters.ExportTypes.SVG,['<svg','<g transform'])
self._exportBox(exporters.ExportTypes.SVG, ['<svg', '<g transform'])
def testAMF(self):
self._exportBox(exporters.ExportTypes.AMF,['<amf units','</object>'])
self._exportBox(exporters.ExportTypes.AMF, ['<amf units', '</object>'])
def testSTEP(self):
self._exportBox(exporters.ExportTypes.STEP,['FILE_SCHEMA'])
self._exportBox(exporters.ExportTypes.STEP, ['FILE_SCHEMA'])
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
"""
#core modules
import StringIO
# core modules
import io
from cadquery import *
from cadquery import exporters
from cadquery import importers
from tests import BaseTest
#where unit test output will be saved
# where unit test output will be saved
import sys
if sys.platform.startswith("win"):
OUTDIR = "c:/temp"
@ -24,24 +24,27 @@ class TestImporters(BaseTest):
:param importType: The type of file we're importing (STEP, STL, etc)
: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:
#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()
#Export the shape to a temporary file
# Export the shape to a temporary file
shape.exportStep(fileName)
# 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")
#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("+Y").size() == 1 and importedShape.faces("+Y").vertices().size() == 4)
self.assertTrue(importedShape.faces("+Z").size() == 1 and importedShape.faces("+Z").vertices().size() == 4)
# 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("+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):
"""
@ -49,6 +52,7 @@ class TestImporters(BaseTest):
"""
self.importBox(importers.ImportTypes.STEP, OUTDIR + "/tempSTEP.step")
if __name__ == '__main__':
import unittest
unittest.main()

View File

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

View File

@ -4,8 +4,9 @@ import unittest
import sys
import os
def readFileAsString(fileName):
f= open(fileName, 'r')
f = open(fileName, 'r')
s = f.read()
f.close()
return s
@ -37,13 +38,16 @@ def toTuple(v):
elif type(v) == Vector:
return v.toTuple()
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):
def assertTupleAlmostEquals(self, expected, actual, places):
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']