diff --git a/cadquery/CQ.py b/cadquery/CQ.py index a898e32..4854944 100644 --- a/cadquery/CQ.py +++ b/cadquery/CQ.py @@ -1261,7 +1261,7 @@ class Workplane(CQ): #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) @@ -1269,6 +1269,7 @@ class Workplane(CQ): #attempt again to consolidate all of the wires c = consolidated.consolidateWires() + return c def mirrorY(self): @@ -1288,7 +1289,6 @@ class Workplane(CQ): Future Enhancements: mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness - """ tm = Matrix() tm.rotateY(math.pi) @@ -1307,7 +1307,6 @@ class Workplane(CQ): Future Enhancements: mirrorX().mirrorY() should work but doesnt, due to some FreeCAD weirdness - """ tm = Matrix() tm.rotateX(math.pi) @@ -1349,11 +1348,9 @@ class Workplane(CQ): If possible, a new object with the results are returned. if not possible, the wires remain separated - FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly somtimes - Additionally, it has a bug where a profile compose of two wires ( rathre than one ) - also does not work properly - - together these are a real problem. + FreeCAD has a bug in Part.Wire([]) which does not create wires/edges properly sometimes + Additionally, it has a bug where a profile compose of two wires ( rather than one ) + also does not work properly. Together these are a real problem. """ wires = self.wires().vals() if len(wires) < 2: diff --git a/cadquery/freecad_impl/geom.py b/cadquery/freecad_impl/geom.py index 7ad2cf1..1b70434 100644 --- a/cadquery/freecad_impl/geom.py +++ b/cadquery/freecad_impl/geom.py @@ -17,9 +17,10 @@ License along with this library; If not, see """ -import math,sys +import math +import cadquery import FreeCAD -#Turns out we don't need the Part module here. +import Part as FreeCADPart def sortWiresByBuildOrder(wireList,plane,result=[]): """ @@ -403,9 +404,9 @@ class Plane: correctly. """ - if isinstance(obj,Vector): + if isinstance(obj, Vector): return Vector(self.fG.multiply(obj.wrapped)) - elif isinstance(obj,Shape): + elif isinstance(obj, cadquery.Shape): return obj.transformShape(self.rG) else: raise ValueError("Dont know how to convert type %s to local coordinates" % str(type(obj))) @@ -464,7 +465,7 @@ class Plane: newP= Plane(self.origin,newXdir,newZdir) return newP - def rotateShapes(self,listOfShapes,rotationMatrix): + def rotateShapes(self, listOfShapes, rotationMatrix): """ rotate the listOfShapes by the rotationMatrix supplied. @param listOfShapes is a list of shape objects @@ -480,24 +481,42 @@ class Plane: #There might be a better way, but to do this rotation takes 3 steps #transform geometry to local coordinates #then rotate about x - #then transform back to global coordiante + #then transform back to global coordinates resultWires = [] for w in listOfShapes: mirrored = w.transformGeometry(rotationMatrix.wrapped) - resultWires.append(mirrored) + + # If the first vertex of the second wire is not coincident with the first or last vertices of the first wire + # we have to fix the wire so that it will mirror correctly + if (mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[0].X and + mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[0].Y and + mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[0].Z) or \ + (mirrored.wrapped.Vertexes[0].X == w.wrapped.Vertexes[-1].X and + mirrored.wrapped.Vertexes[0].Y == w.wrapped.Vertexes[-1].Y and + mirrored.wrapped.Vertexes[0].Z == w.wrapped.Vertexes[-1].Z): + + resultWires.append(mirrored) + else: + # Make sure that our mirrored edges meet up and are ordered properly + aEdges = w.wrapped.Edges + aEdges.extend(mirrored.wrapped.Edges) + comp = FreeCADPart.Compound(aEdges) + mirroredWire = comp.connectEdgesToWires(False).Wires[0] + + resultWires.append(cadquery.Shape.cast(mirroredWire)) return resultWires def _calcTransforms(self): """ - Computes transformation martrices to convert betwene local and global coordinates + Computes transformation martrices to convert between local and global coordinates """ #r is the forward transformation matrix from world to local coordinates #ok i will be really honest-- i cannot understand exactly why this works - #something bout the order of the transaltion and the rotation. - # the double-inverting is strange, and i dont understand it. + #something bout the order of the translation and the rotation. + # the double-inverting is strange, and I don't understand it. r = FreeCAD.Base.Matrix() #forward transform must rotate and adjust for origin diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 822bb3d..fb2645d 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -607,11 +607,64 @@ class TestCadQuery(BaseTest): """ Tests a simple mirroring operation """ - s = Workplane("XY").lineTo(2,2).threePointArc((3,1),(2,0)) \ + s = Workplane("XY").lineTo(2, 2).threePointArc((3, 1), (2, 0)) \ .mirrorX().extrude(0.25) - self.assertEquals(6,s.faces().size()) + self.assertEquals(6, s.faces().size()) self.saveModel(s) + def testUnorderedMirror(self): + """ + Tests whether or not a wire can be mirrored if its mirror won't connect to it + """ + r = 20 + s = 7 + t = 1.5 + + points = [ + (0, t/2), + (r/2-1.5*t, r/2-t), + (s/2, r/2-t), + (s/2, r/2), + (r/2, r/2), + (r/2, s/2), + (r/2-t, s/2), + (r/2-t, r/2-1.5*t), + (t/2, 0) + ] + + r = Workplane("XY").polyline(points).mirrorX() + + self.assertEquals(1, r.wires().size()) + self.assertEquals(16, r.edges().size()) + + # def testChainedMirror(self): + # """ + # Tests whether or not calling mirrorX().mirrorY() works correctly + # """ + # r = 20 + # s = 7 + # t = 1.5 + # + # points = [ + # (0, t/2), + # (r/2-1.5*t, r/2-t), + # (s/2, r/2-t), + # (s/2, r/2), + # (r/2, r/2), + # (r/2, s/2), + # (r/2-t, s/2), + # (r/2-t, r/2-1.5*t), + # (t/2, 0) + # ] + # + # r = Workplane("XY").polyline(points).mirrorX().mirrorY() + # + # self.assertEquals(1, r.wires().size()) + # self.assertEquals(32, r.edges().size()) + + #TODO: Re-work testIbeam test below now that chaining works + #TODO: Add toLocalCoords and toWorldCoords tests + def testIbeam(self): """ Make an ibeam. demonstrates fancy mirroring @@ -623,44 +676,50 @@ class TestCadQuery(BaseTest): t = 1.0 #TODO: for some reason doing 1/4 of the profile and mirroring twice ( .mirrorX().mirrorY() ) - #did not work, due to a bug in freecad-- it was losing edges when createing a composite wire. + #did not work, due to a bug in freecad-- it was losing edges when creating a composite wire. #i just side-stepped it for now pts = [ - (0,H/2.0), - (W/2.0,H/2.0), - (W/2.0,(H/2.0 - t)), - (t/2.0,(H/2.0-t)), - (t/2.0,(t - H/2.0)), - (W/2.0,(t -H/2.0)), - (W/2.0,H/-2.0), - (0,H/-2.0) + (0, H/2.0), + (W/2.0, H/2.0), + (W/2.0, (H/2.0 - t)), + (t/2.0, (H/2.0-t)), + (t/2.0, (t - H/2.0)), + (W/2.0, (t - H/2.0)), + (W/2.0, H / -2.0), + (0, H/-2.0) ] r = s.polyline(pts).mirrorY() #these other forms also work res = r.extrude(L) self.saveModel(res) def testCone(self): - "test that a simple sphere works" - s = Solid.makeCone(0,1.0,2.0) + """ + Tests that a simple cone works + """ + s = Solid.makeCone(0, 1.0, 2.0) t = CQ(s) self.saveModel(t) - self.assertEqual(2,t.faces().size()) + self.assertEqual(2, t.faces().size()) def testFillet(self): - "Tests filleting edges on a solid" + """ + Tests filleting edges on a solid + """ c = CQ( makeUnitCube()).faces(">Z").workplane().circle(0.25).extrude(0.25,True).edges("|Z").fillet(0.2) self.saveModel(c) self.assertEqual(12,c.faces().size() ) def testCounterBores(self): - """Tests making a set of counterbored holes in a face""" + """ + Tests making a set of counterbored holes in a face + """ c = CQ(makeCube(3.0)) - pnts=[ - (-1.0,-1.0),(0.0,0.0),(1.0,1.0) + pnts = [ + (-1.0, -1.0), (0.0, 0.0), (1.0, 1.0) ] - c.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1,0.25,0.25,.75) - self.assertEquals(18,c.faces().size() ) + c.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25, 0.75) + self.assertEquals(18, c.faces().size() ) self.saveModel(c) def testCounterSinks(self): @@ -668,63 +727,67 @@ class TestCadQuery(BaseTest): Tests countersinks """ s = Workplane(Plane.XY()) - result = s.rect(2.0,4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5,3.5,forConstruction=True).vertices().cskHole(0.125, 0.25,82,depth=None) + result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ + .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) self.saveModel(result) def testSplitKeepingHalf(self): - "Tests splitting a solid" + """ + Tests splitting a solid + """ #drill a hole in the side c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() - self.assertEqual(7,c.faces().size() ) + self.assertEqual(7, c.faces().size()) #now cut it in half sideways c.faces(">Y").workplane(-0.5).split(keepTop=True) self.saveModel(c) - self.assertEqual(8,c.faces().size()) + self.assertEqual(8, c.faces().size()) def testSplitKeepingBoth(self): - "Tests splitting a solid" + """ + Tests splitting a solid + """ #drill a hole in the side c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() - self.assertEqual(7,c.faces().size() ) + self.assertEqual(7, c.faces().size()) #now cut it in half sideways - result = c.faces(">Y").workplane(-0.5).split(keepTop=True,keepBottom=True) + result = c.faces(">Y").workplane(-0.5).split(keepTop=True, keepBottom=True) #stack will have both halves, original will be unchanged - self.assertEqual(2, result.solids().size()) #two solids are on the stack, eac - self.assertEqual(8,result.solids().item(0).faces().size()) - self.assertEqual(8,result.solids().item(1).faces().size()) + self.assertEqual(2, result.solids().size()) # two solids are on the stack, eac + self.assertEqual(8, result.solids().item(0).faces().size()) + self.assertEqual(8, result.solids().item(1).faces().size()) def testBoxDefaults(self): """ Tests creating a single box """ - s = Workplane("XY").box(2,3,4) - self.assertEquals(1,s.solids().size() ) + s = Workplane("XY").box(2, 3, 4) + self.assertEquals(1, s.solids().size()) self.saveModel(s) def testSimpleShell(self): """ Create s simple box """ - s = Workplane("XY").box(2,2,2).faces("+Z").shell(0.05) + s = Workplane("XY").box(2, 2, 2).faces("+Z").shell(0.05) self.saveModel(s) - self.assertEquals(23,s.faces().size() ) + self.assertEquals(23, s.faces().size()) def testOpenCornerShell(self): - s = Workplane("XY").box(1,1,1) + s = Workplane("XY").box(1, 1, 1) s1 = s.faces("+Z") s1.add(s.faces("+Y")).add(s.faces("+X")) self.saveModel(s1.shell(0.2)) def testTopFaceFillet(self): - s = Workplane("XY").box(1,1,1).faces("+Z").edges().fillet(0.1) + s = Workplane("XY").box(1, 1, 1).faces("+Z").edges().fillet(0.1) self.assertEquals(s.faces().size(), 10) self.saveModel(s) @@ -732,23 +795,23 @@ class TestCadQuery(BaseTest): """ Tests creating an array of boxes """ - s = Workplane("XY").rect(4.0,4.0,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True) - #1 object, 4 solids beause the object is a compound - self.assertEquals(1,s.solids().size() ) - self.assertEquals(1,s.size()) + s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=True) + #1 object, 4 solids because the object is a compound + self.assertEquals(1, s.solids().size()) + self.assertEquals(1, s.size()) self.saveModel(s) - s = Workplane("XY").rect(4.0,4.0,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=False) - #4 objects, 4 solids, becaue each is a separate solid - self.assertEquals(4,s.size()) - self.assertEquals(4,s.solids().size() ) + s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=False) + #4 objects, 4 solids, because each is a separate solid + self.assertEquals(4, s.size()) + self.assertEquals(4, s.solids().size()) def testBoxCombine(self): - s = Workplane("XY").box(4,4,0.5).faces(">Z").workplane().rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True) + s = Workplane("XY").box(4, 4, 0.5).faces(">Z").workplane().rect(3, 3, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=True) self.saveModel(s) - self.assertEquals(1,s.solids().size()) # we should have one big solid - self.assertEquals(26,s.faces().size()) # should have 26 faces. 6 for the box, and 4x5 for the smaller cubes + self.assertEquals(1, s.solids().size()) # we should have one big solid + self.assertEquals(26, s.faces().size()) # should have 26 faces. 6 for the box, and 4x5 for the smaller cubes def testSphereDefaults(self): s = Workplane("XY").sphere(10) @@ -775,29 +838,29 @@ class TestCadQuery(BaseTest): self.assertEquals(4, s.faces().size()) def testQuickStartXY(self): - s = Workplane(Plane.XY()).box(2,4,0.5).faces(">Z").workplane().rect(1.5,3.5,forConstruction=True)\ - .vertices().cskHole(0.125, 0.25,82,depth=None) - self.assertEquals(1,s.solids().size()) - self.assertEquals(14,s.faces().size()) + s = Workplane(Plane.XY()).box(2, 4, 0.5).faces(">Z").workplane().rect(1.5, 3.5, forConstruction=True)\ + .vertices().cskHole(0.125, 0.25, 82, depth=None) + self.assertEquals(1, s.solids().size()) + self.assertEquals(14, s.faces().size()) self.saveModel(s) def testQuickStartYZ(self): - s = Workplane(Plane.YZ()).box(2,4,0.5).faces(">X").workplane().rect(1.5,3.5,forConstruction=True)\ - .vertices().cskHole(0.125, 0.25,82,depth=None) - self.assertEquals(1,s.solids().size()) - self.assertEquals(14,s.faces().size()) + s = Workplane(Plane.YZ()).box(2, 4, 0.5).faces(">X").workplane().rect(1.5, 3.5, forConstruction=True)\ + .vertices().cskHole(0.125, 0.25, 82, depth=None) + self.assertEquals(1, s.solids().size()) + self.assertEquals(14, s.faces().size()) self.saveModel(s) def testQuickStartXZ(self): - s = Workplane(Plane.XZ()).box(2,4,0.5).faces(">Y").workplane().rect(1.5,3.5,forConstruction=True)\ - .vertices().cskHole(0.125, 0.25,82,depth=None) - self.assertEquals(1,s.solids().size()) - self.assertEquals(14,s.faces().size()) + s = Workplane(Plane.XZ()).box(2, 4, 0.5).faces(">Y").workplane().rect(1.5, 3.5, forConstruction=True)\ + .vertices().cskHole(0.125, 0.25, 82, depth=None) + self.assertEquals(1, s.solids().size()) + self.assertEquals(14, s.faces().size()) self.saveModel(s) def testDoubleTwistedLoft(self): - s = Workplane("XY").polygon(8,20.0).workplane(offset=4.0).transformed(rotate=Vector(0,0,15.0)).polygon(8,20).loft() - s2 = Workplane("XY").polygon(8,20.0).workplane(offset=-4.0).transformed(rotate=Vector(0,0,15.0)).polygon(8,20).loft() + s = Workplane("XY").polygon(8, 20.0).workplane(offset=4.0).transformed(rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() + s2 = Workplane("XY").polygon(8, 20.0).workplane(offset=-4.0).transformed(rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() #self.assertEquals(10,s.faces().size()) #self.assertEquals(1,s.solids().size()) s3 = s.combineSolids(s2) diff --git a/tests/__init__.py b/tests/__init__.py index 2cbd99d..a9e92f8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,43 +3,42 @@ import unittest import sys import os -#from cadquery.freecad_impl.verutil import fc_import -#FreeCAD = fc_import("FreeCAD") -#import cadquery.freecad_impl import FreeCAD -# P = fc_import("FreeCAD.Part") -# V = fc_import("FreeCAD").Base.Vector - import Part as P from FreeCAD import Vector as V + def readFileAsString(fileName): - f= open(fileName,'r') + f= open(fileName, 'r') s = f.read() f.close() return s -def writeStringToFile(strToWrite,fileName): - f = open(fileName,'w') + +def writeStringToFile(strToWrite, fileName): + f = open(fileName, 'w') f.write(strToWrite) f.close() def makeUnitSquareWire(): - return Solid.cast(P.makePolygon([V(0,0,0),V(1,0,0),V(1,1,0),V(0,1,0),V(0,0,0)])) + return Solid.cast(P.makePolygon([V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)])) + def makeUnitCube(): return makeCube(1.0) + def makeCube(size): - return Solid.makeBox(size,size,size) + return Solid.makeBox(size, size, size) + def toTuple(v): - "convert a vector or a vertex to a 3-tuple: x,y,z" + """convert a vector or a vertex to a 3-tuple: x,y,z""" pnt = v if type(v) == FreeCAD.Base.Vector: - return (v.Point.x,v.Point.y,v.Point.z) + return (v.Point.x, v.Point.y, v.Point.z) elif type(v) == Vector: return v.toTuple() else: @@ -48,8 +47,8 @@ def toTuple(v): class BaseTest(unittest.TestCase): - def assertTupleAlmostEquals(self,expected,actual,places): - for i,j in zip(actual,expected): - self.assertAlmostEquals(i,j,places) + def assertTupleAlmostEquals(self, expected, actual, places): + for i, j in zip(actual, expected): + self.assertAlmostEquals(i, j, places) -__all__ = [ 'TestCadObjects','TestCadQuery','TestCQSelectors','TestWorkplanes','TestExporters','TestCQSelectors','TestImporters'] +__all__ = ['TestCadObjects', 'TestCadQuery', 'TestCQSelectors', 'TestWorkplanes', 'TestExporters', 'TestCQSelectors', 'TestImporters']