diff --git a/cadquery/__init__.py b/cadquery/__init__.py index 0e2143a1..af222f62 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -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" diff --git a/cadquery/cq.py b/cadquery/cq.py index 83bb6f1b..0b4b39fc 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -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,23 +330,24 @@ 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:]): raise ValueError("Selected faces must be co-planar.") - if centerOption == 'CenterOfMass': - center = Shape.CombinedCenter(self.objects) - elif centerOption == 'CenterOfBoundBox': - center = Shape.CombinedCenterOfBoundBox(self.objects) + if centerOption == 'CenterOfMass': + center = Shape.CombinedCenter(self.objects) + elif centerOption == 'CenterOfBoundBox': + center = Shape.CombinedCenterOfBoundBox(self.objects) normal = self.objects[0].normalAt() xDir = _computeXdir(normal) @@ -353,38 +356,39 @@ class CQ(object): obj = self.objects[0] if isinstance(obj, Face): - if centerOption == 'CenterOfMass': + if centerOption == 'CenterOfMass': center = obj.Center() - elif centerOption == 'CenterOfBoundBox': + elif centerOption == 'CenterOfBoundBox': center = obj.CenterOfBoundBox() normal = obj.normalAt(center) xDir = _computeXdir(normal) else: if hasattr(obj, 'Center'): - if centerOption == 'CenterOfMass': - center = obj.Center() - elif centerOption == 'CenterOfBoundBox': - center = obj.CenterOfBoundBox() + if centerOption == 'CenterOfMass': + center = obj.Center() + elif centerOption == 'CenterOfBoundBox': + center = obj.CenterOfBoundBox() 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): @@ -743,17 +747,17 @@ class CQ(object): for o in self.objects]) def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)): - """ - Mirror a single CQ object. This operation is the same as in the FreeCAD PartWB's mirroring - - :param mirrorPlane: the plane to mirror about - :type mirrorPlane: string, one of "XY", "YX", "XZ", "ZX", "YZ", "ZY" the planes - :param basePointVector: the base point to mirror about - :type basePointVector: tuple - """ - newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)]) - return newS.first() + """ + Mirror a single CQ object. This operation is the same as in the FreeCAD PartWB's mirroring + :param mirrorPlane: the plane to mirror about + :type mirrorPlane: string, one of "XY", "YX", "XZ", "ZX", "YZ", "ZY" the planes + :param basePointVector: the base point to mirror about + :type basePointVector: tuple + """ + newS = self.newObject( + [self.objects[0].mirror(mirrorPlane, basePointVector)]) + return newS.first() def translate(self, vec): """ @@ -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): @@ -2015,14 +2027,16 @@ class Workplane(CQ): Support for non-prismatic extrusion ( IE, sweeping along a profile, not just perpendicular to the plane extrude to surface. this is quite tricky since the surface selected may not be planar - """ - r = self._extrude(distance,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 @@ -2332,9 +2355,10 @@ class Workplane(CQ): for ws in wireSets: thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) 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) diff --git a/cadquery/cq_directive.py b/cadquery/cq_directive.py index 0dc5fae8..d5db93a7 100644 --- a/cadquery/cq_directive.py +++ b/cadquery/cq_directive.py @@ -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: diff --git a/cadquery/cqgi.py b/cadquery/cqgi.py index 01701dbc..931856b8 100644 --- a/cadquery/cqgi.py +++ b/cadquery/cqgi.py @@ -9,6 +9,7 @@ import cadquery CQSCRIPT = "" + def parse(script_source): """ Parses the script as a model, and returns a model. @@ -48,19 +49,19 @@ class CQModel(object): # TODO: pick up other scirpt metadata: # describe # pick up validation methods - self._find_descriptions() + self._find_descriptions() def _find_vars(self): """ 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. @@ -251,7 +257,7 @@ class InputParameter: if len(self.valid_values) > 0 and new_value not in self.valid_values: raise InvalidParameterError( "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} " - .format(str(new_value), self.name, str(self.valid_values))) + .format(str(new_value), self.name, str(self.valid_values))) if self.varType == NumberParameterType: try: @@ -260,7 +266,7 @@ class InputParameter: except ValueError: raise InvalidParameterError( "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric." - .format(str(new_value), self.name)) + .format(str(new_value), self.name)) elif self.varType == StringParameterType: self.ast_node.s = str(new_value) @@ -283,6 +289,7 @@ class ScriptCallback(object): the build_object() method is exposed to CQ scripts, to allow them 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,30 +408,33 @@ 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): - """ - Called when we see a function call. Is it describe_parameter? - """ - try: + 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) + desc = node.args[1].s + self.cqModel.add_parameter_description(varname, desc) - except: - print "Unable to handle function call" + except: + print("Unable to handle function call") pass - return node + return node + class ConstantAssignmentFinder(ast.NodeTransformer): """ @@ -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 diff --git a/cadquery/freecad_impl/__init__.py b/cadquery/freecad_impl/__init__.py index 05ed9571..c93f87e4 100644 --- a/cadquery/freecad_impl/__init__.py +++ b/cadquery/freecad_impl/__init__.py @@ -37,7 +37,7 @@ def _fc_path(): "/usr/bin/freecad/lib", "/usr/lib/freecad", "/usr/lib64/freecad/lib", - ]: + ]: if os.path.exists(_PATH): return _PATH @@ -80,7 +80,7 @@ def _fc_path(): "c:/apps/FreeCAD 0.15/bin", "c:/apps/FreeCAD 0.16/bin", "c:/apps/FreeCAD 0.17/bin", - ]: + ]: if os.path.exists(_PATH): return _PATH @@ -90,7 +90,7 @@ def _fc_path(): "/Applications/FreeCAD.app/Contents/lib", os.path.join(os.path.expanduser("~"), "Library/Application Support/FreeCAD/lib"), - ]: + ]: if os.path.exists(_PATH): return _PATH diff --git a/cadquery/freecad_impl/exporters.py b/cadquery/freecad_impl/exporters.py index c4b097ad..164fac4b 100644 --- a/cadquery/freecad_impl/exporters.py +++ b/cadquery/freecad_impl/exporters.py @@ -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 = "%s" % 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 @@ -274,21 +281,21 @@ def getSVG(shape,opts=None): for p in visiblePaths: visibleContent += PATHTEMPLATE % p - svg = SVG_TEMPLATE % ( + 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 = """ """ -PATHTEMPLATE="\t\t\t\n" - +PATHTEMPLATE = "\t\t\t\n" diff --git a/cadquery/freecad_impl/geom.py b/cadquery/freecad_impl/geom.py index 2bd8c3ab..a7653157 100644 --- a/cadquery/freecad_impl/geom.py +++ b/cadquery/freecad_impl/geom.py @@ -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 @@ -631,13 +639,13 @@ class BoundBox(object): if (fc_bb1.XMin < fc_bb2.XMin and fc_bb1.XMax > fc_bb2.XMax and fc_bb1.YMin < fc_bb2.YMin and - fc_bb1.YMax > fc_bb2.YMax): + fc_bb1.YMax > fc_bb2.YMax): return b1 if (fc_bb2.XMin < fc_bb1.XMin and fc_bb2.XMax > fc_bb1.XMax and fc_bb2.YMin < fc_bb1.YMin and - fc_bb2.YMax > fc_bb1.YMax): + fc_bb2.YMax > fc_bb1.YMax): return b2 return None diff --git a/cadquery/freecad_impl/importers.py b/cadquery/freecad_impl/importers.py index 7d4f0a96..84d6ddcc 100644 --- a/cadquery/freecad_impl/importers.py +++ b/cadquery/freecad_impl/importers.py @@ -8,10 +8,12 @@ import sys 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 - rshape = Part.read(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,23 +51,26 @@ def importStep(fileName): except: raise ValueError("STEP File Could not be loaded") -#Loads a STEP file from an URL into a CQ.Workplane object -def importStepFromURL(url): - #Now read and return the shape +# Loads a STEP file from an URL into a CQ.Workplane object + + +def importStepFromURL(url): + # Now read and return the shape try: webFile = urlreader.urlopen(url) - tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False) - tempFile.write(webFile.read()) + tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False) + tempFile.write(webFile.read()) webFile.close() - tempFile.close() + tempFile.close() - rshape = Part.read(tempFile.name) + rshape = Part.read(tempFile.name) - #Make sure that we extract all the solids + # Make sure that we extract all the solids solids = [] 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") diff --git a/cadquery/freecad_impl/shapes.py b/cadquery/freecad_impl/shapes.py index 12567e9c..8b8e312e 100644 --- a/cadquery/freecad_impl/shapes.py +++ b/cadquery/freecad_impl/shapes.py @@ -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): @@ -254,12 +257,12 @@ class Shape(object): in FreeCAD >=15, faces no longer have mass, but instead have area. """ if object.wrapped.ShapeType == 'Face': - return object.wrapped.Area + return object.wrapped.Area else: - return object.wrapped.Mass + 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 @@ -393,7 +396,7 @@ class Vertex(Shape): self.Y = obj.Y self.Z = obj.Z - # Helps identify this solid through the use of an ID + # Helps identify this solid through the use of an ID self.label = "" def toTuple(self): @@ -425,12 +428,12 @@ class Edge(Shape): FreeCADPart.Circle: 'CIRCLE' } - # Helps identify this solid through the use of an ID + # Helps identify this solid through the use of an ID self.label = "" 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) @@ -476,7 +479,7 @@ class Edge(Shape): @classmethod def makeCircle(cls, radius, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=360.0, angle2=360): center = Vector(pnt) - normal = Vector(dir) + normal = Vector(dir) return Edge(FreeCADPart.makeCircle(radius, center.wrapped, normal.wrapped, angle1, angle2)) @classmethod @@ -529,7 +532,7 @@ class Wire(Shape): """ self.wrapped = obj - # Helps identify this solid through the use of an ID + # Helps identify this solid through the use of an ID self.label = "" @classmethod @@ -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 @@ -603,12 +609,12 @@ class Face(Shape): FreeCADPart.Cone: 'CONE' } - # Helps identify this solid through the use of an ID + # Helps identify this solid through the use of an ID self.label = "" 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,13 +667,14 @@ class Shell(Shape): """ the outer boundary of a surface """ + def __init__(self, wrapped): """ A Shell """ self.wrapped = wrapped - # Helps identify this solid through the use of an ID + # Helps identify this solid through the use of an ID self.label = "" @classmethod @@ -679,13 +686,14 @@ class Solid(Shape): """ a single solid """ + def __init__(self, obj): """ A Solid """ self.wrapped = obj - # Helps identify this solid through the use of an ID + # Helps identify this solid through the use of an ID self.label = "" @classmethod @@ -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) @@ -1012,7 +1020,7 @@ class Compound(Shape): """ self.wrapped = obj - # Helps identify this solid through the use of an ID + # Helps identify this solid through the use of an ID self.label = "" def Center(self): diff --git a/cadquery/occ_impl/exporters.py b/cadquery/occ_impl/exporters.py index 66f9f061..703ae07b 100644 --- a/cadquery/occ_impl/exporters.py +++ b/cadquery/occ_impl/exporters.py @@ -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,33 +61,31 @@ 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) - + fileLike.write(mesher.toJson()) - + elif exportType == ExportTypes.SVG: fileLike.write(getSVG(shape)) elif exportType == ExportTypes.AMF: @@ -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,109 +148,113 @@ 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') - - #add vertices + 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 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 }; 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() - + points = GCPnts_QuasiUniformDeflection(curve, DISCRETIZATION_TOLERANCE, start, 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): """ @@ -257,56 +266,55 @@ def getPaths(visibleShapes, hiddenShapes): for s in visibleShapes: for e in s.Edges(): visiblePaths.append(makeSVGedge(e)) - + for s in 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 = HLRBRep_Algo() hlr.Add(shape.wrapped) - + projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), DEFAULT_DIR) - ) - + ) + hlr.Projector(projector) hlr.Update() hlr.Hide() - - hlr_shapes = HLRBRep_HLRToShape(hlr.GetHandle()) - + + hlr_shapes = HLRBRep_HLRToShape(hlr.GetHandle()) + visible = [] - + visible_sharp_edges = hlr_shapes.VCompound() if not visible_sharp_edges.IsNull(): visible.append(visible_sharp_edges) - + visible_smooth_edges = hlr_shapes.Rg1LineVCompound() if not visible_smooth_edges.IsNull(): visible.append(visible_smooth_edges) - + visible_contour_edges = hlr_shapes.OutLineVCompound() if not visible_contour_edges.IsNull(): visible.append(visible_contour_edges) @@ -316,31 +324,34 @@ def getSVG(shape,opts=None): hidden_sharp_edges = hlr_shapes.HCompound() if not hidden_sharp_edges.IsNull(): hidden.append(hidden_sharp_edges) - + hidden_contour_edges = hlr_shapes.OutLineHCompound() 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, - hidden) + # 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 @@ -349,21 +360,21 @@ def getSVG(shape,opts=None): for p in visiblePaths: visibleContent += PATHTEMPLATE % p - svg = SVG_TEMPLATE % ( + 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 = """ """ -PATHTEMPLATE="\t\t\t\n" - +PATHTEMPLATE = "\t\t\t\n" diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 297b598c..bfac70ed 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -3,7 +3,7 @@ import cadquery from OCC.gp import gp_Vec, gp_Ax1, gp_Ax3, gp_Pnt, gp_Dir, gp_Trsf, gp, gp_XYZ from OCC.Bnd import Bnd_Box -from OCC.BRepBndLib import brepbndlib_Add # brepbndlib_AddOptimal +from OCC.BRepBndLib import brepbndlib_Add # brepbndlib_AddOptimal from OCC.BRepMesh import BRepMesh_IncrementalMesh TOL = 1e-2 @@ -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,29 +105,33 @@ 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) - + def __sub__(self, v): 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 ''' @@ -134,21 +139,21 @@ class Vector(object): def __ne__(self, other): return self.wrapped.__ne__(other) ''' - + def toPnt(self): - + return gp_Pnt(self.wrapped.XYZ()) - + def toDir(self): - + return gp_Dir(self.wrapped.XYZ()) - def transform(self,T): - - #to gp_Pnt to obey cq transformation convention (in OCC vectors do not translate) + def transform(self, T): + + # to gp_Pnt to obey cq transformation convention (in OCC vectors do not translate) pnt = self.toPnt() pnt_t = pnt.Transformed(T.wrapped) - + return Vector(gp_Vec(pnt_t.XYZ())) @@ -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() @@ -170,19 +176,19 @@ class Matrix: def rotateY(self, angle): self._rotate(gp.OY(), angle) - + def rotateZ(self, angle): self._rotate(gp.OZ(), angle) - - def _rotate(self,direction,angle): - + + def _rotate(self, direction, angle): + new = gp_Trsf() new.SetRotation(direction, angle) - + self.wrapped = self.wrapped * new - + def inverse(self): return Matrix(self.wrapped.Invert()) @@ -199,7 +205,7 @@ class Plane(object): Frequently, it is not necessary to create work planes, as they can be created automatically from faces. """ - + @classmethod def named(cls, stdName, origin=(0, 0, 0)): """Create a predefined Plane based on the conventional names. @@ -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)): @@ -340,11 +346,11 @@ class Plane(object): zDir = Vector(normal) if (zDir.Length == 0.0): raise ValueError('normal should be non null') - + xDir = Vector(xDir) if (xDir.Length == 0.0): raise ValueError('xDir should be non null') - + self.zDir = zDir.normalized() self._setPlaneDir(xDir) self.origin = origin @@ -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) @@ -404,9 +411,9 @@ class Plane(object): * Discretizing points along each curve to provide a more reliable test. """ - + pass - + ''' # TODO: also use a set of points along the wire to test as well. # TODO: would it be more efficient to create objects in the local @@ -425,7 +432,7 @@ class Plane(object): # know if one is inside the other return bb == BoundBox.findOutsideBox2D(bb, tb) ''' - + def toLocalCoords(self, obj): """Project the provided coordinates onto this plane @@ -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 @@ -542,14 +549,14 @@ class Plane(object): resultWires.append(cadquery.Shape.cast(mirroredWire)) 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(), self.xDir.toDir()) T = gp_Trsf() - + if axis == 'X': T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.XDirection())) @@ -558,16 +565,16 @@ class Plane(object): local_coord_system.YDirection())) else: raise NotImplementedError - + resultWires = [] for w in listOfShapes: mirrored = w.transformShape(Matrix(T)) - - #attemp stitching of the wires + + # attemp stitching of the wires resultWires.append(mirrored) - + return resultWires - + def _setPlaneDir(self, xDir): """Set the vectors parallel to the plane, i.e. xDir and yDir""" xDir = Vector(xDir) @@ -586,32 +593,32 @@ class Plane(object): # the double-inverting is strange, and I don't understand it. forward = Matrix() inverse = Matrix() - + global_coord_system = gp_Ax3() local_coord_system = gp_Ax3(gp_Pnt(*self.origin.toTuple()), gp_Dir(*self.zDir.toTuple()), gp_Dir(*self.xDir.toTuple()) ) - + forward.wrapped.SetTransformation(global_coord_system, local_coord_system) 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() - + self.xmin = XMin self.xmax = XMax self.xlen = XMax - XMin @@ -621,11 +628,11 @@ class BoundBox(object): self.zmin = ZMin 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 def add(self, obj, tol=1e-8): @@ -639,11 +646,11 @@ class BoundBox(object): This bounding box is not changed. """ - + tmp = Bnd_Box() tmp.SetGap(tol) tmp.Add(self.wrapped) - + if isinstance(obj, tuple): tmp.Update(*obj) elif isinstance(obj, Vector): @@ -664,23 +671,23 @@ class BoundBox(object): doesn't work correctly plus, there was all kinds of rounding error in the built-in implementation i do not understand. """ - + if (bb1.XMin < bb2.XMin and bb1.XMax > bb2.XMax and bb1.YMin < bb2.YMin and - bb1.YMax > bb2.YMax): + bb1.YMax > bb2.YMax): return bb1 if (bb2.XMin < bb1.XMin and bb2.XMax > bb1.XMax and bb2.YMin < bb1.YMin and - bb2.YMax > bb1.YMax): + bb2.YMax > bb1.YMax): return bb2 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,14 +695,15 @@ 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) def isInside(self, anotherBox): """Is the provided bounding box inside this one?""" - return not anotherBox.wrapped.IsOut(self.wrapped) \ No newline at end of file + return not anotherBox.wrapped.IsOut(self.wrapped) diff --git a/cadquery/occ_impl/importers.py b/cadquery/occ_impl/importers.py index 6863c780..ec1e2a16 100644 --- a/cadquery/occ_impl/importers.py +++ b/cadquery/occ_impl/importers.py @@ -8,10 +8,12 @@ import urllib as urlreader 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,27 +45,30 @@ def importStep(fileName): occ_shapes = [] for i in range(reader.NbShapes()): - occ_shapes.append(reader.Shape(i+1)) - - #Make sure that we extract all the solids + occ_shapes.append(reader.Shape(i + 1)) + + # Make sure that we extract all the solids solids = [] for shape in occ_shapes: solids.append(Shape.cast(shape)) - + return cadquery.Workplane("XY").newObject(solids) except: raise ValueError("STEP File Could not be loaded") -#Loads a STEP file from an URL into a CQ.Workplane object -def importStepFromURL(url): - #Now read and return the shape +# Loads a STEP file from an URL into a CQ.Workplane object + + +def importStepFromURL(url): + # Now read and return the shape try: webFile = urlreader.urlopen(url) tempFile = tempfile.NamedTemporaryFile(suffix='.step', delete=False) tempFile.write(webFile.read()) webFile.close() - tempFile.close() + tempFile.close() return importStep(tempFile.name) except: - raise ValueError("STEP File from the URL: " + url + " Could not be loaded") \ No newline at end of file + raise ValueError("STEP File from the URL: " + + url + " Could not be loaded") diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 77ffcca8..60dbb54f 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -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 -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 +# 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 -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, @@ -48,8 +51,8 @@ from OCC.TopoDS import (topods_Vertex, #downcasting functions 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'} - -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} +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'} -inverse_shape_LUT = {v:k for k,v in shape_LUT.iteritems()} +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} + +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'} +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'} -#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', } @@ -157,9 +160,10 @@ def downcast(topods_obj): ''' Downcasts a TopoDS object to suitable specialized type ''' - + return downcast_LUT[topods_obj.ShapeType()](topods_obj) + class Shape(object): """ Represents a shape in the system. @@ -172,14 +176,14 @@ 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()) @classmethod @@ -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: @@ -216,40 +221,39 @@ class Shape(object): tr = Compound(obj) else: raise ValueError("cast:unknown shape type %s" % s) - ''' - + ''' + return tr # 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): - - - mesh = BRepMesh_IncrementalMesh(self.wrapped,precision,True) + def exportStl(self, fileName, precision=1e-5): + + 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): - + writer = STEPControl_Writer() writer.Transfer(self.wrapped, STEPControl_AsIs) - + return writer.Write(fileName) - + def exportBrep(self, fileName): """ Export given shape to a BREP file """ - + return breptools_Write(self.wrapped, fileName) def exportShape(self, fileName, fileFormat): pass - + def geomType(self): """ Gets the underlying geometry type @@ -274,16 +278,15 @@ class Shape(object): Compound: 'Compound' Wire: 'Wire' """ - - tr = geom_LUT[self.wrapped.ShapeType()] - - if type(tr) is str: + + tr = geom_LUT[self.wrapped.ShapeType()] + + if type(tr) is str: return tr 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) @@ -324,47 +327,48 @@ class Shape(object): if type(basePointVector) == tuple: basePointVector = Vector(basePointVector) - + T = gp_Trsf() T.SetMirror(gp_Ax2(gp_Pnt(*basePointVector.toTuple()), - mirrorPlaneNormalVector)) - + mirrorPlaneNormalVector)) + return Shape.cast(self.wrapped.Transformed(T)) @staticmethod def _center_of_mass(shape): - + Properties = GProp_GProps() brepgprop_VolumeProperties(shape, Properties) - + return Vector(Properties.CentreOfMass()) def Center(self): ''' Center of mass ''' - + 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): @@ -373,13 +377,13 @@ class Shape(object): """ Properties = GProp_GProps() 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 - + @staticmethod def centerOfMass(obj): """ @@ -387,15 +391,15 @@ class Shape(object): """ Properties = GProp_GProps() 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,30 +413,29 @@ 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() def ShapeType(self): return shape_LUT[self.wrapped.ShapeType()] - - - def _entities(self,topo_type): - - out = {} #using dict to prevent duplicates - + + def _entities(self, topo_type): + + out = {} # using dict to prevent duplicates + explorer = TopExp_Explorer(self.wrapped, inverse_shape_LUT[topo_type]) - + while explorer.More(): item = explorer.Current() - out[item.__hash__()] = item # some implementations use __hash__ + out[item.__hash__()] = item # some implementations use __hash__ explorer.Next() - - return out.values() + + return list(out.values()) def Vertices(self): @@ -458,9 +461,9 @@ 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, True).Shape()) @@ -478,34 +481,34 @@ class Shape(object): if type(endVector) == tuple: endVector = Vector(endVector) - + T = gp_Trsf() T.SetRotation(gp_Ax1(startVector.toPnt(), (endVector - startVector).toDir()), angleDegrees) - + return self._apply_transform(T) def translate(self, vector): if type(vector) == tuple: vector = Vector(vector) - + T = gp_Trsf() T.SetTranslation(vector.wrapped) - + return self._apply_transform(T) def scale(self, factor): - + T = gp_Trsf() T.SetScale(gp_Pnt(), factor) - + return self._apply_transform(T) def copy(self): - + return Shape.cast(BRepBuilderAPI_Copy(self.wrapped).Shape()) def transformShape(self, tMatrix): @@ -518,7 +521,7 @@ class Shape(object): r = Shape.cast(BRepBuilderAPI_Transform(self.wrapped, tMatrix.wrapped).Shape()) r.forConstruction = self.forConstruction - + return r def transformGeometry(self, tMatrix): @@ -538,12 +541,12 @@ class Shape(object): gp_GTrsf(tMatrix.wrapped), True).Shape()) r.forConstruction = self.forConstruction - + return r def __hash__(self): return self.hashCode() - + def cut(self, toCut): """ Remove a shape from another one @@ -555,13 +558,13 @@ 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()) def intersect(self, toIntersect): @@ -570,14 +573,15 @@ class Shape(object): """ return Shape.cast(BRepAlgoAPI_Common(self.wrapped, toIntersect.wrapped).Shape()) - + def to_html(self): """ Jupyter 3D representation support - """ - + """ + raise NotImplemented + class Vertex(Shape): """ A Single Point in Space @@ -587,13 +591,13 @@ 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() def toTuple(self): - + geom_point = BRep_Tool.Pnt(self.wrapped) return (geom_point.X(), geom_point.Y(), @@ -604,23 +608,24 @@ class Vertex(Shape): The center of a vertex is itself! """ return Vector(self.toTuple()) - + @classmethod - def makeVertex(cls,x,y,z): - - return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x,y,z) - ).Vertex()) + def makeVertex(cls, x, y, z): + + return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z) + ).Vertex()) 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): """ @@ -640,10 +644,10 @@ class Edge(Shape, Mixin1D): Note, circles may have the start and end points the same """ - + curve = self._geomAdaptor() umin = curve.FirstParameter() - + return Vector(curve.Value(umin)) def endPoint(self): @@ -654,10 +658,10 @@ class Edge(Shape, Mixin1D): Note, circles may have the start and end points the same """ - + curve = self._geomAdaptor() umax = curve.LastParameter() - + return Vector(curve.Value(umax)) def tangentAt(self, locationVector=None): @@ -666,53 +670,54 @@ class Edge(Shape, Mixin1D): :param locationVector: location to use. Use the center point if None :return: tangent vector """ - + curve = self._geomAdaptor() - + if locationVector: raise NotImplementedError else: umin, umax = curve.FirstParameter(), curve.LastParameter() - umid = 0.5*(umin+umax) - - curve_props = BRepLProp_CLProps(curve, 2, curve.Tolerance()) #TODO what are good parameters for those? + umid = 0.5 * (umin + umax) + + # 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) def Center(self): - + Properties = GProp_GProps() brepgprop_LinearProperties(self.wrapped, Properties) - + return Vector(Properties.CentreOfMass()) @classmethod def makeCircle(cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angle1=360.0, angle2=360): """ - + """ pnt = Vector(pnt) dir = Vector(dir) - + circle_gp = gp_Circ(gp_Ax2(pnt.toPnt(), 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()) - + @classmethod def makeSpline(cls, listOfVector): """ @@ -721,11 +726,12 @@ 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() - + return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge()) @classmethod @@ -741,7 +747,7 @@ class Edge(Shape, Mixin1D): circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2.toPnt(), v3.toPnt()).Value() - + return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) @classmethod @@ -770,11 +776,11 @@ class Wire(Shape, Mixin1D): :param listOfWires: :return: """ - + wire_builder = BRepBuilderAPI_MakeWire() for wire in listOfWires: wire_builder.Add(wire.wrapped) - + return cls(wire_builder.Wire()) @classmethod @@ -788,7 +794,7 @@ class Wire(Shape, Mixin1D): wire_builder = BRepBuilderAPI_MakeWire() for edge in listOfEdges: wire_builder.Add(edge.wrapped) - + return cls(wire_builder.Wire()) @classmethod @@ -800,8 +806,8 @@ class Wire(Shape, Mixin1D): :param normal: vector representing the direction of the plane the circle should lie in :return: """ - - circle_edge = Edge.makeCircle(radius,center,normal) + + circle_edge = Edge.makeCircle(radius, center, normal) w = cls.assembleEdges([circle_edge]) return w @@ -809,58 +815,60 @@ class Wire(Shape, Mixin1D): def makePolygon(cls, listOfVertices, forConstruction=False): # convert list of tuples into Vectors. wire_builder = BRepBuilderAPI_MakePolygon() - - for v in listOfVertices: wire_builder.Add(v.toPnt()) - - w = cls(wire_builder.Wire()) + + for v in listOfVertices: + wire_builder.Add(v.toPnt()) + + w = cls(wire_builder.Wire()) w.forConstruction = forConstruction - + 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)) - - #3. put it together into a wire - n_turns = height/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 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() wire_builder.Add(topods_Wire(self.wrapped)) wire_builder.Add(topods_Wire(other.wrapped)) wire_builder.Build() - + return self.__class__(wire_builder.Wire()) + class Face(Shape): """ a bounded surface that represents part of the boundary of a solid @@ -870,12 +878,12 @@ 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): - + return breptools_UVBounds(self.wrapped) - + def normalAt(self, locationVector=None): """ Computes the normal vector at the desired location on the face. @@ -884,47 +892,46 @@ 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) - - def Center(self): - + + def Center(self): + Properties = GProp_GProps() brepgprop_SurfaceProperties(self.wrapped, Properties) - + return Vector(Properties.CentreOfMass()) @classmethod def makePlane(cls, length, width, basePnt=(0, 0, 0), dir=(0, 0, 1)): 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): @@ -933,32 +940,33 @@ class Face(Shape): Create a ruled surface out of two edges or wires. If wires are used then 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: return cls.cast(brepfill_Face(edgeOrWire1.wrapped, edgeOrWire1.wrapped)) - + @classmethod def makeFromWires(cls, outerWire, innerWires=[]): ''' 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 @@ -966,19 +974,19 @@ class Shell(Shape): @classmethod def makeShell(cls, listOfFaces): - + shell_wrapped = TopoDS_Shell() shell_builder = TopoDS_Builder() shell_builder.MakeShell(shell_wrapped) - + for face in listOfFaces: shell_builder.Add(face.wrapped) - + return cls(shell_wrapped) class Mixin3D(object): - + def tessellate(self, tolerance): return self.wrapped.tessellate(tolerance) @@ -990,12 +998,12 @@ class Mixin3D(object): :return: Filleted solid """ nativeEdges = [e.wrapped for e in edgeList] - + 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()) def chamfer(self, length, length2, edgeList): @@ -1008,30 +1016,30 @@ 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, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map) - - # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API - chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) - + + # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API + chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) + if length2: d1 = length d2 = length2 else: d1 = length d2 = length - + for e in nativeEdges: face = edge_face_map.FindFromKey(e).First() 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): @@ -1043,21 +1051,22 @@ class Mixin3D(object): :param tolerance: modelling tolerance of the method, default=0.0001 :return: a shelled solid """ - + occ_faces_list = TopTools_ListOfShape() for f in faceList: - occ_faces_list.Append(f.wrapped) - + occ_faces_list.Append(f.wrapped) + shell_builder = BRepOffsetAPI_MakeThickSolid(self.wrapped, occ_faces_list, thickness, - tolerance) - + tolerance) + shell_builder.Build() - + return self.__class__(shell_builder.Shape()) -class Solid(Shape,Mixin3D): + +class Solid(Shape, Mixin3D): """ a single solid """ @@ -1093,11 +1102,11 @@ class Solid(Shape,Mixin3D): dir=Vector(0,0,1) and angle=360' """ return cls(BRepPrimAPI_MakeCone(gp_Ax2(pnt.toPnt(), - dir.toDir()), + dir.toDir()), 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): @@ -1136,16 +1145,16 @@ class Solid(Shape,Mixin3D): """ # the True flag requests building a solid instead of a shell. loft_builder = BRepOffsetAPI_ThruSections(True, ruled) - + for w in listOfWire: loft_builder.AddWire(w.wrapped) - + loft_builder.Build() 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()) - - @classmethod - def _extrudeAuxSpine(cls,wire,spine,auxSpine): + angleDegrees1 * DEG2RAD, + angleDegrees2 * DEG2RAD, + angleDegrees3 * DEG2RAD).Shape()) + + @classmethod + 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,39 +1220,39 @@ 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, radius, center=vecCenter, dir=vecNormal).wrapped - + # extrude the outer wire outer_solid = cls._extrudeAuxSpine(outerWire.wrapped, straight_spine_w, aux_spine_w) - + # extrude inner wires inner_solids = [cls._extrudeAuxSpine(w.wrapped, straight_spine_w. aux_spine_w) for w in innerWires] - + # 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 - - 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()) + comp_builder.MakeCompound(inner_comp) # TODO this could be not needed + + 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()) - @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()) @@ -1307,14 +1317,14 @@ class Solid(Shape,Mixin3D): reliable. """ face = Face.makeFromWires(outerWire, innerWires) - + v1 = Vector(axisStart) v2 = Vector(axisEnd) v2 = v2 - v1 - revol_builder = BRepPrimAPI_MakeRevol(face.wrapped, - gp_Ax1(v1.toPnt(),v2.toDir()), - angleDegrees*DEG2RAD, - True) + revol_builder = BRepPrimAPI_MakeRevol(face.wrapped, + 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) @@ -1344,14 +1354,15 @@ class Solid(Shape,Mixin3D): builder.Add(w.wrapped) builder.Build() - + return cls(builder.Shape()) -class Compound(Shape,Mixin3D): + +class Compound(Shape, Mixin3D): """ a collection of disconnected solids """ - + @classmethod def makeCompound(cls, listOfShapes): """ @@ -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 - - for s in listOfShapes: comp_builder.Add(comp,s.wrapped) - + comp_builder.MakeCompound(comp) # TODO this could be not needed + + 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. @@ -1382,33 +1396,34 @@ def sortWiresByBuildOrder(wireList, plane, result=[]): Returns, list of lists. """ - - #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:]) - - #use FixOrientation + # 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:]) + + # 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)} - - #construct the result + result = {w: outer_inner_map.Find( + w.wrapped) for w in all_wires if outer_inner_map.IsBound(w.wrapped)} + + # 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(): tmp.append(Wire(iterator.Value())) iterator.Next() - + rv.append(tmp) - - return rv \ No newline at end of file + + return rv diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 0f17869b..a7bfc3ea 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -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,41 +345,44 @@ class DirectionNthSelector(ParallelDirSelector): Linear Edges Planar Faces """ + def __init__(self, vector, n, directionMax=True, tolerance=0.0001): self.direction = vector self.max = max self.directionMax = directionMax 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) - #make a distance to object dict - #this is one to many mapping so I am using a default dict with list + # 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 objectDict = defaultdict(list) - for el in objectList: - objectDict[round(distance(el),digits)].append(el) - + for el in objectList: + 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 @@ -414,189 +443,199 @@ 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(['>','<']) - - #index definition - ix_number = Group(Optional('-')+Word(nums)) + + # direction operator + direction_op = oneOf(['>', '<']) + + # 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(['|','#','+','-']) - - #named view - named_view = oneOf(['front','back','left','right','top','bottom']) - - return direction('only_dir') | \ - (type_op('type_op') + cqtype('cq_type')) | \ - (direction_op('dir_op') + direction('dir') + Optional(index)) | \ - (other_op('other_op') + direction('dir')) | \ - named_view('named_view') -_grammar = _makeGrammar() #make a grammar instance + index = lsqbracket + ix_number('index') + rsqbracket + + # other operators + other_op = oneOf(['|', '#', '+', '-']) + + # named view + named_view = oneOf(['front', 'back', 'left', 'right', 'top', 'bottom']) + + return direction('only_dir') | \ + (type_op('type_op') + cqtype('cq_type')) | \ + (direction_op('dir_op') + direction('dir') + Optional(index)) | \ + (other_op('other_op') + direction('dir')) | \ + named_view('named_view') + + +_grammar = _makeGrammar() # make a grammar instance + class _SimpleStringSyntaxSelector(Selector): """ 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 """ if 'only_dir' in pr: vec = self._getVector(pr) return DirectionSelector(vec) - + elif 'type_op' in pr: - return TypeSelector(pr.cq_type) - + return TypeSelector(pr.cq_type) + elif 'dir_op' in pr: vec = self._getVector(pr) 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) return self.operator[pr.other_op](vec) - + else: 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 [+\|-\|<\|>\|] \ """ 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']) + or_op = Literal('or') + 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 - - #define callback functions for all operations + + # construct a simple selector from every matched + atom.setParseAction(atom_callback) + + # 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 @@ -627,10 +666,10 @@ class StringSyntaxSelector(Selector): curve/surface type (same as :py:class:`TypeSelector`) ***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ`` or ``(x,y,z)`` which defines an arbitrary direction - + It is possible to combine simple selectors together using logical operations. The following operations are suuported - + :and: Logical AND, e.g. >X and >Y :or: @@ -642,22 +681,23 @@ class StringSyntaxSelector(Selector): Finally, it is also possible to use even more complex expressions with nesting and arbitrary number of terms, e.g. - + (not >X[0] and #XY) or >XY[0] 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 """ self.selectorString = selectorString parse_result = _expression_grammar.parseString(selectorString, - parseAll=True) + 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 """ - return self.mySelector.filter(objectList) \ No newline at end of file + return self.mySelector.filter(objectList) diff --git a/tests/TestCQGI.py b/tests/TestCQGI.py index 7cc967fe..0aafebc0 100644 --- a/tests/TestCQGI.py +++ b/tests/TestCQGI.py @@ -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,8 +90,8 @@ 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)) \ No newline at end of file + self.assertEqual(2, len(model.metadata.parameters)) diff --git a/tests/TestCQSelectors.py b/tests/TestCQSelectors.py index 1bc411b1..48b1e3a1 100644 --- a/tests/TestCQSelectors.py +++ b/tests/TestCQSelectors.py @@ -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 + 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("(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) - - #2nd face with inversed selection vector + self.assertAlmostEqual(val.Center().x, -1.5) + + # 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[-2]').val() - self.assertAlmostEqual(val.Center().x,1.5) - - #Last face + self.assertAlmostEqual(val.Center().x, 1.5) + + # Last face val = c.faces('>X[-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) - - #test selection of multiple faces with the same distance + 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) + + # test selection of multiple faces with the same distance c = Workplane('XY')\ - .box(1,4,1,centered=(False,True,False)).faces('Z')\ - .box(1,1,1,centered=(True,True,False)) - - #select 2nd from the bottom (NB python indexing is 0-based) + .box(1, 4, 1, centered=(False, True, False)).faces('Z')\ + .box(1, 1, 1, centered=(True, True, False)) + + # 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) - - #do the same but by selecting 3rd from the top + self.assertAlmostEqual(val.Center().z, 1) + + # do the same but by selecting 3rd from the top vals = c.faces('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) - + 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,13 +398,14 @@ 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 v = c.vertices(">X and >Y").vals() self.assertEqual(2, len(v)) @@ -402,7 +425,7 @@ class TestCQSelectors(BaseTest): self.assertEqual(2, len(fl)) el = c.edges(S("|X") + S("|Y")).vals() self.assertEqual(8, len(el)) - + # test using extended string syntax fl = c.faces(">Z or X")).vals() self.assertEqual(3, len(fl)) - + # test using extended string syntax fl = c.faces("#Z exc >X").vals() self.assertEqual(3, len(fl)) @@ -440,43 +463,42 @@ class TestCQSelectors(BaseTest): self.assertEqual(5, len(fl)) el = c.faces('>Z').edges(-S('>X')).vals() self.assertEqual(3, len(el)) - + # test using extended string syntax fl = c.faces('not >Z').vals() self.assertEqual(5, len(fl)) el = c.faces('>Z').edges('not >X').vals() self.assertEqual(3, len(el)) - + def testComplexStringSelector(self): c = CQ(makeUnitCube()) - + v = c.vertices('(>X and >Y) or ((0,0,1)) exc XY and (Z or X)', 'not ( X or Y )'] - for e in expressions: gram.parseString(e,parseAll=True) - \ No newline at end of file + for e in expressions: + gram.parseString(e, parseAll=True) diff --git a/tests/TestCadObjects.py b/tests/TestCadObjects.py index e594d44a..807d1779 100644 --- a/tests/TestCadObjects.py +++ b/tests/TestCadObjects.py @@ -1,4 +1,4 @@ -#system modules +# system modules import sys import unittest from tests import BaseTest @@ -6,16 +6,17 @@ from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_DZ from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex, BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace) - + 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,34 +34,39 @@ 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() - + 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)) + def testTranslate(self): + 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() diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index e2551e38..69d6c9a6 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -2,23 +2,26 @@ This module tests cadquery creation and manipulation functions """ -#system modules -import math,sys,os.path,time +# system modules +import math +import sys +import os.path +import time -#my modules +# my modules from cadquery import * from cadquery import exporters -from tests import BaseTest,writeStringToFile,makeUnitCube,readFileAsString,makeUnitSquareWire,makeCube +from tests import BaseTest, writeStringToFile, makeUnitCube, readFileAsString, makeUnitSquareWire, makeCube -#where unit test output will be saved +# where unit test output will be saved import sys if sys.platform.startswith("win"): OUTDIR = "c:/temp" else: OUTDIR = "/tmp" -SUMMARY_FILE = os.path.join(OUTDIR,"testSummary.html") +SUMMARY_FILE = os.path.join(OUTDIR, "testSummary.html") -SUMMARY_TEMPLATE=""" +SUMMARY_TEMPLATE = """