""" This module tests cadquery creation and manipulation functions """ # system modules import math, os.path, time, tempfile from random import choice from random import random from random import randrange # my modules from cadquery import * from cadquery import exporters from tests import ( BaseTest, writeStringToFile, makeUnitCube, readFileAsString, makeUnitSquareWire, makeCube, ) # where unit test output will be saved OUTDIR = tempfile.gettempdir() SUMMARY_FILE = os.path.join(OUTDIR, "testSummary.html") SUMMARY_TEMPLATE = """ """ TEST_RESULT_TEMPLATE = """

%(name)s

%(svg)s
""" # clean up any summary file that is in the output directory. # i know, this sux, but there is no other way to do this in 2.6, as we cannot do class fixutres till 2.7 writeStringToFile(SUMMARY_TEMPLATE, SUMMARY_FILE) class TestCadQuery(BaseTest): def tearDown(self): """ Update summary with data from this test. This is a really hackey way of doing it-- we get a startup event from module load, but there is no way in unittest to get a single shutdown event-- except for stuff in 2.7 and above So what we do here is to read the existing file, stick in more content, and leave it """ svgFile = os.path.join(OUTDIR, self._testMethodName + ".svg") # all tests do not produce output if os.path.exists(svgFile): existingSummary = readFileAsString(SUMMARY_FILE) svgText = readFileAsString(svgFile) svgText = svgText.replace( '', "" ) # now write data into the file # the content we are replacing it with also includes the marker, so it can be replaced again existingSummary = existingSummary.replace( "", TEST_RESULT_TEMPLATE % (dict(svg=svgText, name=self._testMethodName)), ) writeStringToFile(existingSummary, SUMMARY_FILE) def saveModel(self, shape): """ shape must be a CQ object Save models in SVG and STEP format """ shape.exportSvg(os.path.join(OUTDIR, self._testMethodName + ".svg")) shape.val().exportStep(os.path.join(OUTDIR, self._testMethodName + ".step")) def testToOCC(self): """ Tests to make sure that a CadQuery object is converted correctly to a OCC object. """ r = Workplane("XY").rect(5, 5).extrude(5) r = r.toOCC() import OCC.Core as OCC self.assertEqual(type(r), OCC.TopoDS.TopoDS_Compound) def testToSVG(self): """ Tests to make sure that a CadQuery object is converted correctly to SVG """ r = Workplane("XY").rect(5, 5).extrude(5) r_str = r.toSvg() # Make sure that a couple of sections from the SVG output make sense self.assertTrue(r_str.index('path d="M') > 0) self.assertTrue( r_str.index('line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3"') > 0 ) def testCubePlugin(self): """ Tests a plugin that combines cubes together with a base :return: """ # make the plugin method def makeCubes(self, length): # self refers to the CQ or Workplane object # inner method that creates a cube def _singleCube(pnt): # pnt is a location in local coordinates # since we're using eachpoint with useLocalCoordinates=True return Solid.makeBox(length, length, length, pnt) # use CQ utility method to iterate over the stack, call our # method, and convert to/from local coordinates. return self.eachpoint(_singleCube, True) # link the plugin in Workplane.makeCubes = makeCubes # call it result = ( Workplane("XY") .box(6.0, 8.0, 0.5) .faces(">Z") .rect(4.0, 4.0, forConstruction=True) .vertices() ) result = result.makeCubes(1.0) result = result.combineSolids() self.saveModel(result) self.assertEqual(1, result.solids().size()) def testCylinderPlugin(self): """ Tests a cylinder plugin. The plugin creates cylinders of the specified radius and height for each item on the stack This is a very short plugin that illustrates just about the simplest possible plugin """ def cylinders(self, radius, height): def _cyl(pnt): # inner function to build a cylinder return Solid.makeCylinder(radius, height, pnt) # combine all the cylinders into a single compound r = self.eachpoint(_cyl, True).combineSolids() return r Workplane.cyl = cylinders # now test. here we want weird workplane to see if the objects are transformed right s = ( Workplane(Plane(Vector((0, 0, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)))) .rect(2.0, 3.0, forConstruction=True) .vertices() .cyl(0.25, 0.5) ) self.assertEqual(4, s.solids().size()) self.saveModel(s) def testPolygonPlugin(self): """ Tests a plugin to make regular polygons around points on the stack Demonstratings using eachpoint to allow working in local coordinates to create geometry """ def rPoly(self, nSides, diameter): def _makePolygon(center): # pnt is a vector in local coordinates angle = 2.0 * math.pi / nSides pnts = [] for i in range(nSides + 1): pnts.append( center + Vector( (diameter / 2.0 * math.cos(angle * i)), (diameter / 2.0 * math.sin(angle * i)), 0, ) ) return Wire.makePolygon(pnts) return self.eachpoint(_makePolygon, True) Workplane.rPoly = rPoly s = ( Workplane("XY") .box(4.0, 4.0, 0.25) .faces(">Z") .workplane() .rect(2.0, 2.0, forConstruction=True) .vertices() .rPoly(5, 0.5) .cutThruAll() ) # 6 base sides, 4 pentagons, 5 sides each = 26 self.assertEqual(26, s.faces().size()) self.saveModel(s) def testPointList(self): """ Tests adding points and using them """ c = CQ(makeUnitCube()) s = c.faces(">Z").workplane().pushPoints([(-0.3, 0.3), (0.3, 0.3), (0, 0)]) self.assertEqual(3, s.size()) # TODO: is the ability to iterate over points with circle really worth it? # maybe we should just require using all() and a loop for this. the semantics and # possible combinations got too hard ( ie, .circle().circle() ) was really odd body = s.circle(0.05).cutThruAll() self.saveModel(body) self.assertEqual(9, body.faces().size()) # Test the case when using eachpoint with only a blank workplane def callback_fn(pnt): self.assertEqual((0.0, 0.0), (pnt.x, pnt.y)) r = Workplane("XY") r.objects = [] r.eachpoint(callback_fn) def testWorkplaneFromFace(self): # make a workplane on the top face s = CQ(makeUnitCube()).faces(">Z").workplane() r = s.circle(0.125).cutBlind(-2.0) self.saveModel(r) # the result should have 7 faces self.assertEqual(7, r.faces().size()) self.assertEqual(type(r.val()), Compound) self.assertEqual(type(r.first().val()), Compound) def testFrontReference(self): # make a workplane on the top face s = CQ(makeUnitCube()).faces("front").workplane() r = s.circle(0.125).cutBlind(-2.0) self.saveModel(r) # the result should have 7 faces self.assertEqual(7, r.faces().size()) self.assertEqual(type(r.val()), Compound) self.assertEqual(type(r.first().val()), Compound) def testRotate(self): """Test solid rotation at the CQ object level.""" box = Workplane("XY").box(1, 1, 5) box.rotate((0, 0, 0), (1, 0, 0), 90) startPoint = box.faces("Z") .circle(1.5) .workplane(offset=3.0) .rect(0.75, 0.5) .loft(combine=True) ) self.saveModel(s) # self.assertEqual(1,s.solids().size() ) # self.assertEqual(8,s.faces().size() ) def testRevolveCylinder(self): """ Test creating a solid using the revolve operation. :return: """ # The dimensions of the model. These can be modified rather than changing the # shape's code directly. rectangle_width = 10.0 rectangle_length = 10.0 angle_degrees = 360.0 # Test revolve without any options for making a cylinder result = ( Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) # Test revolve when only setting the angle to revolve through result = ( Workplane("XY") .rect(rectangle_width, rectangle_length, False) .revolve(angle_degrees) ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) result = ( Workplane("XY") .rect(rectangle_width, rectangle_length, False) .revolve(270.0) ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) # Test when passing revolve the angle and the axis of revolution's start point result = ( Workplane("XY") .rect(rectangle_width, rectangle_length) .revolve(angle_degrees, (-5, -5)) ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) result = ( Workplane("XY") .rect(rectangle_width, rectangle_length) .revolve(270.0, (-5, -5)) ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) # Test when passing revolve the angle and both the start and ends of the axis of revolution result = ( Workplane("XY") .rect(rectangle_width, rectangle_length) .revolve(angle_degrees, (-5, -5), (-5, 5)) ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) result = ( Workplane("XY") .rect(rectangle_width, rectangle_length) .revolve(270.0, (-5, -5), (-5, 5)) ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) # Testing all of the above without combine result = ( Workplane("XY") .rect(rectangle_width, rectangle_length) .revolve(angle_degrees, (-5, -5), (-5, 5), False) ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) result = ( Workplane("XY") .rect(rectangle_width, rectangle_length) .revolve(270.0, (-5, -5), (-5, 5), False) ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) def testRevolveDonut(self): """ Test creating a solid donut shape with square walls :return: """ # The dimensions of the model. These can be modified rather than changing the # shape's code directly. rectangle_width = 10.0 rectangle_length = 10.0 angle_degrees = 360.0 result = ( Workplane("XY") .rect(rectangle_width, rectangle_length, True) .revolve(angle_degrees, (20, 0), (20, 10)) ) self.assertEqual(4, result.faces().size()) self.assertEqual(4, result.vertices().size()) self.assertEqual(6, result.edges().size()) def testRevolveCone(self): """ Test creating a solid from a revolved triangle :return: """ result = Workplane("XY").lineTo(0, 10).lineTo(5, 0).close().revolve() self.assertEqual(2, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(2, result.edges().size()) def testSpline(self): """ Tests construction of splines """ pts = [(0, 0), (0, 1), (1, 2), (2, 4)] # Spline path - just a smoke test path = Workplane("XZ").spline(pts).val() # Closed spline path_closed = Workplane("XZ").spline(pts, periodic=True).val() self.assertTrue(path_closed.IsClosed()) # attempt to build a valid face w = Wire.assembleEdges([path_closed,]) f = Face.makeFromWires(w) self.assertTrue(f.isValid()) # attempt to build an invalid face w = Wire.assembleEdges([path,]) f = Face.makeFromWires(w) self.assertFalse(f.isValid()) # Spline with explicit tangents path_const = Workplane("XZ").spline(pts, tangents=((0, 1), (1, 0))).val() self.assertFalse(path.tangentAt(0) == path_const.tangentAt(0)) self.assertFalse(path.tangentAt(1) == path_const.tangentAt(1)) # test include current path1 = Workplane("XZ").spline(pts[1:], includeCurrent=True).val() self.assertAlmostEqual(path.Length(), path1.Length()) def testSweep(self): """ Tests the operation of sweeping a wire(s) along a path """ pts = [(0, 0), (0, 1), (1, 2), (2, 4)] # Spline path path = Workplane("XZ").spline(pts) # Test defaults result = Workplane("XY").circle(1.0).sweep(path) self.assertEqual(3, result.faces().size()) self.assertEqual(3, result.edges().size()) # Test with makeSolid False result = Workplane("XY").circle(1.0).sweep(path, makeSolid=False) self.assertEqual(1, result.faces().size()) self.assertEqual(3, result.edges().size()) # Test with isFrenet True result = Workplane("XY").circle(1.0).sweep(path, isFrenet=True) self.assertEqual(3, result.faces().size()) self.assertEqual(3, result.edges().size()) # Test with makeSolid False and isFrenet True result = Workplane("XY").circle(1.0).sweep(path, makeSolid=False, isFrenet=True) self.assertEqual(1, result.faces().size()) self.assertEqual(3, result.edges().size()) # Test rectangle with defaults result = Workplane("XY").rect(1.0, 1.0).sweep(path) self.assertEqual(6, result.faces().size()) self.assertEqual(12, result.edges().size()) # Polyline path path = Workplane("XZ").polyline(pts) # Test defaults result = Workplane("XY").circle(0.1).sweep(path, transition="transformed") self.assertEqual(5, result.faces().size()) self.assertEqual(7, result.edges().size()) # Polyline path and one inner profiles path = Workplane("XZ").polyline(pts) # Test defaults result = ( Workplane("XY") .circle(0.2) .circle(0.1) .sweep(path, transition="transformed") ) self.assertEqual(8, result.faces().size()) self.assertEqual(14, result.edges().size()) # Polyline path and different transition settings for t in ("transformed", "right", "round"): path = Workplane("XZ").polyline(pts) result = ( Workplane("XY") .circle(0.2) .rect(0.2, 0.1) .rect(0.1, 0.2) .sweep(path, transition=t) ) self.assertTrue(result.solids().val().isValid()) # Polyline path and multiple inner profiles path = Workplane("XZ").polyline(pts) # Test defaults result = ( Workplane("XY") .circle(0.2) .rect(0.2, 0.1) .rect(0.1, 0.2) .circle(0.1) .sweep(path) ) self.assertTrue(result.solids().val().isValid()) # Arc path path = Workplane("XZ").threePointArc((1.0, 1.5), (0.0, 1.0)) # Test defaults result = Workplane("XY").circle(0.1).sweep(path) self.assertEqual(3, result.faces().size()) self.assertEqual(3, result.edges().size()) def testMultisectionSweep(self): """ Tests the operation of sweeping along a list of wire(s) along a path """ # X axis line length 20.0 path = Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 defaultSweep = ( Workplane("YZ") .workplane(offset=-10.0) .circle(2.0) .workplane(offset=10.0) .circle(1.0) .workplane(offset=10.0) .circle(2.0) .sweep(path, multisection=True) ) # We can sweep thrue different shapes recttocircleSweep = ( Workplane("YZ") .workplane(offset=-10.0) .rect(2.0, 2.0) .workplane(offset=8.0) .circle(1.0) .workplane(offset=4.0) .circle(1.0) .workplane(offset=8.0) .rect(2.0, 2.0) .sweep(path, multisection=True) ) circletorectSweep = ( Workplane("YZ") .workplane(offset=-10.0) .circle(1.0) .workplane(offset=7.0) .rect(2.0, 2.0) .workplane(offset=6.0) .rect(2.0, 2.0) .workplane(offset=7.0) .circle(1.0) .sweep(path, multisection=True) ) # Placement of the Shape is important otherwise could produce unexpected shape specialSweep = ( Workplane("YZ") .circle(1.0) .workplane(offset=10.0) .rect(2.0, 2.0) .sweep(path, multisection=True) ) # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 path = ( Workplane("XZ") .moveTo(-5, 4) .lineTo(0, 4) .threePointArc((4, 0), (0, -4)) .lineTo(-5, -4) ) # Placement of different shapes should follow the path # cylinder r=1.5 along first line # then sweep allong arc from r=1.5 to r=1.0 # then cylinder r=1.0 along last line arcSweep = ( Workplane("YZ") .workplane(offset=-5) .moveTo(0, 4) .circle(1.5) .workplane(offset=5) .circle(1.5) .moveTo(0, -8) .circle(1.0) .workplane(offset=-5) .circle(1.0) .sweep(path, multisection=True) ) # Test and saveModel self.assertEqual(1, defaultSweep.solids().size()) self.assertEqual(1, circletorectSweep.solids().size()) self.assertEqual(1, recttocircleSweep.solids().size()) self.assertEqual(1, specialSweep.solids().size()) self.assertEqual(1, arcSweep.solids().size()) self.saveModel(defaultSweep) def testTwistExtrude(self): """ Tests extrusion while twisting through an angle. """ profile = Workplane("XY").rect(10, 10) r = profile.twistExtrude(10, 45, False) self.assertEqual(6, r.faces().size()) def testTwistExtrudeCombine(self): """ Tests extrusion while twisting through an angle, combining with other solids. """ profile = Workplane("XY").rect(10, 10) r = profile.twistExtrude(10, 45) self.assertEqual(6, r.faces().size()) def testRectArray(self): NUMX = 3 NUMY = 3 s = ( Workplane("XY") .box(40, 40, 5, centered=(True, True, True)) .faces(">Z") .workplane() .rarray(8.0, 8.0, NUMX, NUMY, True) .circle(2.0) .extrude(2.0) ) # s = Workplane("XY").box(40,40,5,centered=(True,True,True)).faces(">Z").workplane().circle(2.0).extrude(2.0) self.saveModel(s) # 6 faces for the box, 2 faces for each cylinder self.assertEqual(6 + NUMX * NUMY * 2, s.faces().size()) def testPolarArray(self): radius = 10 # Test for proper number of elements s = Workplane("XY").polarArray(radius, 0, 180, 1) self.assertEqual(1, s.size()) s = Workplane("XY").polarArray(radius, 0, 180, 6) self.assertEqual(6, s.size()) # Test for proper placement when fill == True s = Workplane("XY").polarArray(radius, 0, 180, 3) self.assertAlmostEqual(0, s.objects[1].x) self.assertAlmostEqual(radius, s.objects[1].y) # Test for proper placement when angle to fill is multiple of 360 deg s = Workplane("XY").polarArray(radius, 0, 360, 4) self.assertAlmostEqual(0, s.objects[1].x) self.assertAlmostEqual(radius, s.objects[1].y) # Test for proper placement when fill == False s = Workplane("XY").polarArray(radius, 0, 90, 3, fill=False) self.assertAlmostEqual(0, s.objects[1].x) self.assertAlmostEqual(radius, s.objects[1].y) # Test for proper operation of startAngle s = Workplane("XY").polarArray(radius, 90, 180, 3) self.assertAlmostEqual(0, s.objects[0].x) self.assertAlmostEqual(radius, s.objects[0].y) def testNestedCircle(self): s = ( Workplane("XY") .box(40, 40, 5) .pushPoints([(10, 0), (0, 10)]) .circle(4) .circle(2) .extrude(4) ) self.saveModel(s) self.assertEqual(14, s.faces().size()) def testLegoBrick(self): # test making a simple lego brick # which of the below # inputs lbumps = 8 wbumps = 2 # lego brick constants P = 8.0 # nominal pitch c = 0.1 # clearance on each brick side H = 1.2 * P # nominal height of a brick bumpDiam = 4.8 # the standard bump diameter # the nominal thickness of the walls, normally 1.5 t = (P - (2 * c) - bumpDiam) / 2.0 postDiam = P - t # works out to 6.5 total_length = lbumps * P - 2.0 * c total_width = wbumps * P - 2.0 * c # build the brick s = Workplane("XY").box(total_length, total_width, H) # make the base s = s.faces("Z") .workplane() .rarray(P, P, lbumps, wbumps, True) .circle(bumpDiam / 2.0) .extrude(1.8) ) # make the bumps on the top # add posts on the bottom. posts are different diameter depending on geometry # solid studs for 1 bump, tubes for multiple, none for 1x1 # this is cheating a little-- how to select the inner face from the shell? tmp = s.faces(" 1 and wbumps > 1: tmp = ( tmp.rarray(P, P, lbumps - 1, wbumps - 1, center=True) .circle(postDiam / 2.0) .circle(bumpDiam / 2.0) .extrude(H - t) ) elif lbumps > 1: tmp = tmp.rarray(P, P, lbumps - 1, 1, center=True).circle(t).extrude(H - t) elif wbumps > 1: tmp = tmp.rarray(P, P, 1, wbumps - 1, center=True).circle(t).extrude(H - t) self.saveModel(s) def testAngledHoles(self): s = ( Workplane("front") .box(4.0, 4.0, 0.25) .faces(">Z") .workplane() .transformed(offset=Vector(0, -1.5, 1.0), rotate=Vector(60, 0, 0)) .rect(1.5, 1.5, forConstruction=True) .vertices() .hole(0.25) ) self.saveModel(s) self.assertEqual(10, s.faces().size()) def testTranslateSolid(self): c = CQ(makeUnitCube()) self.assertAlmostEqual(0.0, c.faces("Z").workplane().circle(0.125).extrude(0.5, True) ) # make a boss, not updating the original self.assertEqual(8, r.faces().size()) # just the boss faces self.assertEqual(6, c.faces().size()) # original is not modified def testSolidReferencesCombineTrue(self): s = Workplane(Plane.XY()) r = s.rect(2.0, 2.0).extrude(0.5) # the result of course has 6 faces self.assertEqual(6, r.faces().size()) # the original workplane does not, because it did not have a solid initially self.assertEqual(0, s.faces().size()) t = r.faces(">Z").workplane().rect(0.25, 0.25).extrude(0.5, True) # of course the result has 11 faces self.assertEqual(11, t.faces().size()) # r (being the parent) remains unmodified self.assertEqual(6, r.faces().size()) self.saveModel(r) def testSolidReferenceCombineFalse(self): s = Workplane(Plane.XY()) r = s.rect(2.0, 2.0).extrude(0.5) # the result of course has 6 faces self.assertEqual(6, r.faces().size()) # the original workplane does not, because it did not have a solid initially self.assertEqual(0, s.faces().size()) t = r.faces(">Z").workplane().rect(0.25, 0.25).extrude(0.5, False) # result has 6 faces, becuase it was not combined with the original self.assertEqual(6, t.faces().size()) self.assertEqual(6, r.faces().size()) # original is unmodified as well # subseuent opertions use that context solid afterwards def testSimpleWorkplane(self): """ A simple square part with a hole in it """ s = Workplane(Plane.XY()) r = ( s.rect(2.0, 2.0) .extrude(0.5) .faces(">Z") .workplane() .circle(0.25) .cutBlind(-1.0) ) self.saveModel(r) self.assertEqual(7, r.faces().size()) def testMultiFaceWorkplane(self): """ Test Creation of workplane from multiple co-planar face selection. """ s = Workplane("XY").box(1, 1, 1).faces(">Z").rect(1, 0.5).cutBlind(-0.2) w = s.faces(">Z").workplane() o = w.objects[0] # origin of the workplane self.assertAlmostEqual(o.x, 0.0, 3) self.assertAlmostEqual(o.y, 0.0, 3) self.assertAlmostEqual(o.z, 0.5, 3) def testTriangularPrism(self): s = Workplane("XY").lineTo(1, 0).lineTo(1, 1).close().extrude(0.2) self.saveModel(s) def testMultiWireWorkplane(self): """ A simple square part with a hole in it-- but this time done as a single extrusion with two wires, as opposed to s cut """ s = Workplane(Plane.XY()) r = s.rect(2.0, 2.0).circle(0.25).extrude(0.5) self.saveModel(r) self.assertEqual(7, r.faces().size()) def testConstructionWire(self): """ Tests a wire with several holes, that are based on the vertices of a square also tests using a workplane plane other than XY """ s = Workplane(Plane.YZ()) r = ( s.rect(2.0, 2.0) .rect(1.3, 1.3, forConstruction=True) .vertices() .circle(0.125) .extrude(0.5) ) self.saveModel(r) # 10 faces-- 6 plus 4 holes, the vertices of the second rect. self.assertEqual(10, r.faces().size()) def testTwoWorkplanes(self): """ Tests a model that uses more than one workplane """ # base block s = Workplane(Plane.XY()) # TODO: this syntax is nice, but the iteration might not be worth # the complexity. # the simpler and slightly longer version would be: # r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices() # for c in r.all(): # c.circle(0.125).extrude(0.5,True) r = ( s.rect(2.0, 2.0) .rect(1.3, 1.3, forConstruction=True) .vertices() .circle(0.125) .extrude(0.5) ) # side hole, blind deep 1.9 t = r.faces(">Y").workplane().circle(0.125).cutBlind(-1.9) self.saveModel(t) self.assertEqual(12, t.faces().size()) def testCut(self): """ Tests the cut function by itself to catch the case where a Solid object is passed. """ s = Workplane(Plane.XY()) currentS = s.rect(2.0, 2.0).extrude(0.5) toCut = s.rect(1.0, 1.0).extrude(0.5) resS = currentS.cut(toCut.val()) self.assertEqual(10, resS.faces().size()) def testIntersect(self): """ Tests the intersect function. """ s = Workplane(Plane.XY()) currentS = s.rect(2.0, 2.0).extrude(0.5) toIntersect = s.rect(1.0, 1.0).extrude(1) resS = currentS.intersect(toIntersect.val()) self.assertEqual(6, resS.faces().size()) self.assertAlmostEqual(resS.val().Volume(), 0.5) resS = currentS.intersect(toIntersect) self.assertEqual(6, resS.faces().size()) self.assertAlmostEqual(resS.val().Volume(), 0.5) def testBoundingBox(self): """ Tests the boudingbox center of a model """ result0 = ( Workplane("XY") .moveTo(10, 0) .lineTo(5, 0) .threePointArc((3.9393, 0.4393), (3.5, 1.5)) .threePointArc((3.0607, 2.5607), (2, 3)) .lineTo(1.5, 3) .threePointArc((0.4393, 3.4393), (0, 4.5)) .lineTo(0, 13.5) .threePointArc((0.4393, 14.5607), (1.5, 15)) .lineTo(28, 15) .lineTo(28, 13.5) .lineTo(24, 13.5) .lineTo(24, 11.5) .lineTo(27, 11.5) .lineTo(27, 10) .lineTo(22, 10) .lineTo(22, 13.2) .lineTo(14.5, 13.2) .lineTo(14.5, 10) .lineTo(12.5, 10) .lineTo(12.5, 13.2) .lineTo(5.5, 13.2) .lineTo(5.5, 2) .threePointArc((5.793, 1.293), (6.5, 1)) .lineTo(10, 1) .close() ) result = result0.extrude(100) bb_center = result.val().BoundingBox().center self.saveModel(result) self.assertAlmostEqual(14.0, bb_center.x, 3) self.assertAlmostEqual(7.5, bb_center.y, 3) self.assertAlmostEqual(50.0, bb_center.z, 3) # The following will raise with the default tolerance of TOL 1e-2 bb = result.val().BoundingBox(tolerance=1e-3) self.assertAlmostEqual(0.0, bb.xmin, 2) self.assertAlmostEqual(28, bb.xmax, 2) self.assertAlmostEqual(0.0, bb.ymin, 2) self.assertAlmostEqual(15.0, bb.ymax, 2) self.assertAlmostEqual(0.0, bb.zmin, 2) self.assertAlmostEqual(100.0, bb.zmax, 2) def testCutThroughAll(self): """ Tests a model that uses more than one workplane """ # base block s = Workplane(Plane.XY()) r = ( s.rect(2.0, 2.0) .rect(1.3, 1.3, forConstruction=True) .vertices() .circle(0.125) .extrude(0.5) ) # thru all without explicit face selection t = r.circle(0.5).cutThruAll() self.assertEqual(11, t.faces().size()) # side hole, thru all t = t.faces(">Y").workplane().circle(0.125).cutThruAll() self.saveModel(t) self.assertEqual(13, t.faces().size()) def testCutToFaceOffsetNOTIMPLEMENTEDYET(self): """ Tests cutting up to a given face, or an offset from a face """ # base block s = Workplane(Plane.XY()) r = ( s.rect(2.0, 2.0) .rect(1.3, 1.3, forConstruction=True) .vertices() .circle(0.125) .extrude(0.5) ) # side hole, up to 0.1 from the last face try: t = ( r.faces(">Y") .workplane() .circle(0.125) .cutToOffsetFromFace(r.faces().mminDist(Dir.Y), 0.1) ) # should end up being a blind hole self.assertEqual(10, t.faces().size()) t.first().val().exportStep("c:/temp/testCutToFace.STEP") except: pass # Not Implemented Yet def testWorkplaneOnExistingSolid(self): "Tests extruding on an existing solid" c = ( CQ(makeUnitCube()) .faces(">Z") .workplane() .circle(0.25) .circle(0.125) .extrude(0.25) ) self.saveModel(c) self.assertEqual(10, c.faces().size()) def testWorkplaneCenterMove(self): # this workplane is centered at x=0.5,y=0.5, the center of the upper face s = ( Workplane("XY").box(1, 1, 1).faces(">Z").workplane().center(-0.5, -0.5) ) # move the center to the corner t = s.circle(0.25).extrude(0.2) # make a boss self.assertEqual(9, t.faces().size()) self.saveModel(t) def testBasicLines(self): "Make a triangluar boss" global OUTDIR s = Workplane(Plane.XY()) # TODO: extrude() should imply wire() if not done already # most users dont understand what a wire is, they are just drawing r = s.lineTo(1.0, 0).lineTo(0, 1.0).close().wire().extrude(0.25) r.val().exportStep(os.path.join(OUTDIR, "testBasicLinesStep1.STEP")) # no faces on the original workplane self.assertEqual(0, s.faces().size()) # 5 faces on newly created object self.assertEqual(5, r.faces().size()) # now add a circle through a side face r1 = r.faces("+XY").workplane().circle(0.08).cutThruAll() self.assertEqual(6, r1.faces().size()) r1.val().exportStep(os.path.join(OUTDIR, "testBasicLinesXY.STEP")) # now add a circle through a top r2 = r1.faces("+Z").workplane().circle(0.08).cutThruAll() self.assertEqual(9, r2.faces().size()) r2.val().exportStep(os.path.join(OUTDIR, "testBasicLinesZ.STEP")) self.saveModel(r2) def test2DDrawing(self): """ Draw things like 2D lines and arcs, should be expanded later to include all 2D constructs """ s = Workplane(Plane.XY()) r = ( s.lineTo(1.0, 0.0) .lineTo(1.0, 1.0) .threePointArc((1.0, 1.5), (0.0, 1.0)) .lineTo(0.0, 0.0) .moveTo(1.0, 0.0) .lineTo(2.0, 0.0) .lineTo(2.0, 2.0) .threePointArc((2.0, 2.5), (0.0, 2.0)) .lineTo(-2.0, 2.0) .lineTo(-2.0, 0.0) .close() ) self.assertEqual(1, r.wires().size()) # Test the *LineTo functions s = Workplane(Plane.XY()) r = s.hLineTo(1.0).vLineTo(1.0).hLineTo(0.0).close() self.assertEqual(1, r.wire().size()) self.assertEqual(4, r.edges().size()) # Test the *Line functions s = Workplane(Plane.XY()) r = s.hLine(1.0).vLine(1.0).hLine(-1.0).close() self.assertEqual(1, r.wire().size()) self.assertEqual(4, r.edges().size()) # Test the move function s = Workplane(Plane.XY()) r = s.move(1.0, 1.0).hLine(1.0).vLine(1.0).hLine(-1.0).close() self.assertEqual(1, r.wire().size()) self.assertEqual(4, r.edges().size()) self.assertEqual( (1.0, 1.0), ( r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) .first() .val() .X, r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) .first() .val() .Y, ), ) # Test the sagittaArc and radiusArc functions a1 = Workplane(Plane.YZ()).threePointArc((5, 1), (10, 0)) a2 = Workplane(Plane.YZ()).sagittaArc((10, 0), -1) a3 = Workplane(Plane.YZ()).threePointArc((6, 2), (12, 0)) a4 = Workplane(Plane.YZ()).radiusArc((12, 0), -10) assert a1.edges().first().val().geomType() == "CIRCLE" assert a2.edges().first().val().geomType() == "CIRCLE" assert a3.edges().first().val().geomType() == "CIRCLE" assert a4.edges().first().val().geomType() == "CIRCLE" assert a1.edges().first().val().Length() == a2.edges().first().val().Length() assert a3.edges().first().val().Length() == a4.edges().first().val().Length() def testPolarLines(self): """ Draw some polar lines and check expected results """ # Test the PolarLine* functions s = Workplane(Plane.XY()) r = ( s.polarLine(10, 45) .polarLineTo(10, -45) .polarLine(10, -180) .polarLine(-10, -90) .close() ) # a single wire, 5 edges self.assertEqual(1, r.wires().size()) self.assertEqual(5, r.wires().edges().size()) def testLargestDimension(self): """ Tests the largestDimension function when no solids are on the stack and when there are """ r = Workplane("XY").box(1, 1, 1) dim = r.largestDimension() self.assertAlmostEqual(8.7, dim, 1) r = Workplane("XY") dim = r.largestDimension() self.assertEqual(-1, dim) def testOccBottle(self): """ Make the OCC bottle example. """ L = 20.0 w = 6.0 t = 3.0 s = Workplane(Plane.XY()) # draw half the profile of the bottle p = ( s.center(-L / 2.0, 0) .vLine(w / 2.0) .threePointArc((L / 2.0, w / 2.0 + t), (L, w / 2.0)) .vLine(-w / 2.0) .mirrorX() .extrude(30.0, True) ) # make the neck p.faces(">Z").workplane().circle(3.0).extrude( 2.0, True ) # .edges().fillet(0.05) # make a shell p.faces(">Z").shell(0.3) self.saveModel(p) def testSplineShape(self): """ Tests making a shape with an edge that is a spline """ s = Workplane(Plane.XY()) sPnts = [ (2.75, 1.5), (2.5, 1.75), (2.0, 1.5), (1.5, 1.0), (1.0, 1.25), (0.5, 1.0), (0, 1.0), ] r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts).close() r = r.extrude(0.5) self.saveModel(r) def testSimpleMirror(self): """ Tests a simple mirroring operation """ s = ( Workplane("XY") .lineTo(2, 2) .threePointArc((3, 1), (2, 0)) .mirrorX() .extrude(0.25) ) self.assertEqual(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, 0), (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.assertEqual(1, r.wires().size()) self.assertEqual(18, r.edges().size()) # try the same with includeCurrent=True r = Workplane("XY").polyline(points[1:], includeCurrent=True).mirrorX() self.assertEqual(1, r.wires().size()) self.assertEqual(18, r.edges().size()) def testChainedMirror(self): """ Tests whether or not calling mirrorX().mirrorY() works correctly """ r = 20 s = 7 t = 1.5 points = [ (0, 0), (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().extrude(1).faces(">Z") 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 """ s = Workplane(Plane.XY()) L = 100.0 H = 20.0 W = 20.0 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 creating a composite wire. # i just side-stepped it for now pts = [ (0, 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): """ 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()) def testFillet(self): """ 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 testChamfer(self): """ Test chamfer API with a box shape """ cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1) self.saveModel(cube) self.assertEqual(10, cube.faces().size()) def testChamferAsymmetrical(self): """ Test chamfer API with a box shape for asymmetrical lengths """ cube = CQ(makeUnitCube()).faces(">Z").chamfer(0.1, 0.2) self.saveModel(cube) self.assertEqual(10, cube.faces().size()) # test if edge lengths are different edge = cube.edges(">Z").vals()[0] self.assertAlmostEqual(0.6, edge.Length(), 3) edge = cube.edges("|Z").vals()[0] self.assertAlmostEqual(0.9, edge.Length(), 3) def testChamferCylinder(self): """ Test chamfer API with a cylinder shape """ cylinder = Workplane("XY").circle(1).extrude(1).faces(">Z").chamfer(0.1) self.saveModel(cylinder) self.assertEqual(4, cylinder.faces().size()) def testCounterBores(self): """ 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)] c = c.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25, 0.75) self.assertEqual(18, c.faces().size()) self.saveModel(c) # Tests the case where the depth of the cboreHole is not specified c2 = CQ(makeCube(3.0)) pnts = [(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)] c2 = c2.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25) self.assertEqual(15, c2.faces().size()) def testCounterSinks(self): """ 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) ) self.saveModel(result) def testSplitKeepingHalf(self): """ 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()) # now cut it in half sideways result = c.faces(">Y").workplane(-0.5).split(keepTop=True) self.saveModel(result) self.assertEqual(8, result.faces().size()) def testSplitKeepingBoth(self): """ 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()) # now cut it in half sideways result = c.faces(">Y").workplane(-0.5).split(keepTop=True, keepBottom=True) # stack will have both halves, original will be unchanged # two solids are on the stack, eac self.assertEqual(2, result.solids().size()) self.assertEqual(8, result.solids().item(0).faces().size()) self.assertEqual(8, result.solids().item(1).faces().size()) def testSplitKeepingBottom(self): """ Tests splitting a solid improperly """ # Drill a hole in the side c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() self.assertEqual(7, c.faces().size()) # Now cut it in half sideways result = c.faces(">Y").workplane(-0.5).split(keepTop=False, keepBottom=True) # stack will have both halves, original will be unchanged # one solid is on the stack self.assertEqual(1, result.solids().size()) self.assertEqual(8, result.solids().item(0).faces().size()) def testBoxDefaults(self): """ Tests creating a single box """ s = Workplane("XY").box(2, 3, 4) self.assertEqual(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) self.saveModel(s) self.assertEqual(23, s.faces().size()) def testOpenCornerShell(self): 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)) # Tests the list option variation of add s1 = s.faces("+Z") s1.add(s.faces("+Y")).add([s.faces("+X")]) # Tests the raw object option variation of add s1 = s.faces("+Z") s1.add(s.faces("+Y")).add(s.faces("+X").val().wrapped) def testTopFaceFillet(self): s = Workplane("XY").box(1, 1, 1).faces("+Z").edges().fillet(0.1) self.assertEqual(s.faces().size(), 10) self.saveModel(s) def testBoxPointList(self): """ 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 because the object is a compound self.assertEqual(4, s.solids().size()) self.assertEqual(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, because each is a separate solid self.assertEqual(4, s.size()) self.assertEqual(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) ) self.saveModel(s) self.assertEqual(1, s.solids().size()) # we should have one big solid # should have 26 faces. 6 for the box, and 4x5 for the smaller cubes self.assertEqual(26, s.faces().size()) def testSphereDefaults(self): s = Workplane("XY").sphere(10) self.saveModel(s) # Until FreeCAD fixes their sphere operation self.assertEqual(1, s.solids().size()) self.assertEqual(1, s.faces().size()) def testSphereCustom(self): s = Workplane("XY").sphere( 10, angle1=0, angle2=90, angle3=360, centered=(False, False, False) ) self.saveModel(s) self.assertEqual(1, s.solids().size()) self.assertEqual(2, s.faces().size()) def testSpherePointList(self): s = ( Workplane("XY") .rect(4.0, 4.0, forConstruction=True) .vertices() .sphere(0.25, combine=False) ) # self.saveModel(s) # Until FreeCAD fixes their sphere operation self.assertEqual(4, s.solids().size()) self.assertEqual(4, s.faces().size()) def testSphereCombine(self): s = ( Workplane("XY") .rect(4.0, 4.0, forConstruction=True) .vertices() .sphere(2.25, combine=True) ) # self.saveModel(s) # Until FreeCAD fixes their sphere operation self.assertEqual(1, s.solids().size()) self.assertEqual(4, s.faces().size()) def testWedgeDefaults(self): s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5) self.saveModel(s) self.assertEqual(1, s.solids().size()) self.assertEqual(5, s.faces().size()) self.assertEqual(5, s.vertices().size()) def testWedgeCentering(self): s = Workplane("XY").wedge( 10, 10, 10, 5, 5, 5, 5, centered=(False, False, False) ) # self.saveModel(s) self.assertEqual(1, s.solids().size()) self.assertEqual(5, s.faces().size()) self.assertEqual(5, s.vertices().size()) def testWedgePointList(self): s = ( Workplane("XY") .rect(4.0, 4.0, forConstruction=True) .vertices() .wedge(10, 10, 10, 5, 5, 5, 5, combine=False) ) # self.saveModel(s) self.assertEqual(4, s.solids().size()) self.assertEqual(20, s.faces().size()) self.assertEqual(20, s.vertices().size()) def testWedgeCombined(self): s = ( Workplane("XY") .rect(4.0, 4.0, forConstruction=True) .vertices() .wedge(10, 10, 10, 5, 5, 5, 5, combine=True) ) # self.saveModel(s) self.assertEqual(1, s.solids().size()) self.assertEqual(12, s.faces().size()) self.assertEqual(16, s.vertices().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.assertEqual(1, s.solids().size()) self.assertEqual(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.assertEqual(1, s.solids().size()) self.assertEqual(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.assertEqual(1, s.solids().size()) self.assertEqual(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() ) # self.assertEquals(10,s.faces().size()) # self.assertEquals(1,s.solids().size()) s3 = s.combineSolids(s2) self.saveModel(s3) def testTwistedLoft(self): s = ( Workplane("XY") .polygon(8, 20.0) .workplane(offset=4.0) .transformed(rotate=Vector(0, 0, 15.0)) .polygon(8, 20) .loft() ) self.assertEqual(10, s.faces().size()) self.assertEqual(1, s.solids().size()) self.saveModel(s) def testUnions(self): # duplicates a memory problem of some kind reported when combining lots of objects s = Workplane("XY").rect(0.5, 0.5).extrude(5.0) o = [] beginTime = time.time() for i in range(15): t = Workplane("XY").center(10.0 * i, 0).rect(0.5, 0.5).extrude(5.0) o.append(t) # union stuff for oo in o: s = s.union(oo) print("Total time %0.3f" % (time.time() - beginTime)) # Test unioning a Solid object s = Workplane(Plane.XY()) currentS = s.rect(2.0, 2.0).extrude(0.5) toUnion = s.rect(1.0, 1.0).extrude(1.0) resS = currentS.union(toUnion) self.assertEqual(11, resS.faces().size()) def testCombine(self): s = Workplane(Plane.XY()) objects1 = s.rect(2.0, 2.0).extrude(0.5).faces(">Z").rect(1.0, 1.0).extrude(0.5) objects1.combine() self.assertEqual(11, objects1.faces().size()) def testCombineSolidsInLoop(self): # duplicates a memory problem of some kind reported when combining lots of objects s = Workplane("XY").rect(0.5, 0.5).extrude(5.0) o = [] beginTime = time.time() for i in range(15): t = Workplane("XY").center(10.0 * i, 0).rect(0.5, 0.5).extrude(5.0) o.append(t) # append the 'good way' for oo in o: s.add(oo) s = s.combineSolids() print("Total time %0.3f" % (time.time() - beginTime)) self.saveModel(s) def testClean(self): """ Tests the `clean()` method which is called automatically. """ # make a cube with a splitter edge on one of the faces # autosimplify should remove the splitter s = ( Workplane("XY") .moveTo(0, 0) .line(5, 0) .line(5, 0) .line(0, 10) .line(-10, 0) .close() .extrude(10) ) self.assertEqual(6, s.faces().size()) # test removal of splitter caused by union operation s = Workplane("XY").box(10, 10, 10).union(Workplane("XY").box(20, 10, 10)) self.assertEqual(6, s.faces().size()) # test removal of splitter caused by extrude+combine operation s = ( Workplane("XY") .box(10, 10, 10) .faces(">Y") .workplane() .rect(5, 10, 5) .extrude(20) ) self.assertEqual(10, s.faces().size()) # test removal of splitter caused by double hole operation s = ( Workplane("XY") .box(10, 10, 10) .faces(">Z") .workplane() .hole(3, 5) .faces(">Z") .workplane() .hole(3, 10) ) self.assertEqual(7, s.faces().size()) # test removal of splitter caused by cutThruAll s = ( Workplane("XY") .box(10, 10, 10) .faces(">Y") .workplane() .rect(10, 5) .cutBlind(-5) .faces(">Z") .workplane() .center(0, 2.5) .rect(5, 5) .cutThruAll() ) self.assertEqual(18, s.faces().size()) # test removal of splitter with box s = Workplane("XY").box(5, 5, 5).box(10, 5, 2) self.assertEqual(14, s.faces().size()) def testNoClean(self): """ Test the case when clean is disabled. """ # test disabling autoSimplify s = ( Workplane("XY") .moveTo(0, 0) .line(5, 0) .line(5, 0) .line(0, 10) .line(-10, 0) .close() .extrude(10, clean=False) ) self.assertEqual(7, s.faces().size()) s = ( Workplane("XY") .box(10, 10, 10) .union(Workplane("XY").box(20, 10, 10), clean=False) ) self.assertEqual(14, s.faces().size()) s = ( Workplane("XY") .box(10, 10, 10) .faces(">Y") .workplane() .rect(5, 10, 5) .extrude(20, clean=False) ) self.assertEqual(12, s.faces().size()) def testExplicitClean(self): """ Test running of `clean()` method explicitly. """ s = ( Workplane("XY") .moveTo(0, 0) .line(5, 0) .line(5, 0) .line(0, 10) .line(-10, 0) .close() .extrude(10, clean=False) .clean() ) self.assertEqual(6, s.faces().size()) def testPlanes(self): """ Test other planes other than the normal ones (XY, YZ) """ # ZX plane s = Workplane(Plane.ZX()) 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) # YX plane s = Workplane(Plane.YX()) 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) # YX plane s = Workplane(Plane.YX()) 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) # ZY plane s = Workplane(Plane.ZY()) 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) # front plane s = Workplane(Plane.front()) 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) # back plane s = Workplane(Plane.back()) 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) # left plane s = Workplane(Plane.left()) 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) # right plane s = Workplane(Plane.right()) 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) # top plane s = Workplane(Plane.top()) 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) # bottom plane s = Workplane(Plane.bottom()) 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 testIsInside(self): """ Testing if one box is inside of another. """ box1 = Workplane(Plane.XY()).box(10, 10, 10) box2 = Workplane(Plane.XY()).box(5, 5, 5) self.assertFalse(box2.val().BoundingBox().isInside(box1.val().BoundingBox())) self.assertTrue(box1.val().BoundingBox().isInside(box2.val().BoundingBox())) def testCup(self): """ UOM = "mm" # # PARAMETERS and PRESETS # These parameters can be manipulated by end users # bottomDiameter = FloatParam(min=10.0,presets={'default':50.0,'tumbler':50.0,'shot':35.0,'tea':50.0,'saucer':100.0},group="Basics", desc="Bottom diameter") topDiameter = FloatParam(min=10.0,presets={'default':85.0,'tumbler':85.0,'shot':50.0,'tea':51.0,'saucer':400.0 },group="Basics", desc="Top diameter") thickness = FloatParam(min=0.1,presets={'default':2.0,'tumbler':2.0,'shot':2.66,'tea':2.0,'saucer':2.0},group="Basics", desc="Thickness") height = FloatParam(min=1.0,presets={'default':80.0,'tumbler':80.0,'shot':59.0,'tea':125.0,'saucer':40.0},group="Basics", desc="Overall height") lipradius = FloatParam(min=1.0,presets={'default':1.0,'tumbler':1.0,'shot':0.8,'tea':1.0,'saucer':1.0},group="Basics", desc="Lip Radius") bottomThickness = FloatParam(min=1.0,presets={'default':5.0,'tumbler':5.0,'shot':10.0,'tea':10.0,'saucer':5.0},group="Basics", desc="BottomThickness") # # Your build method. It must return a solid object # def build(): br = bottomDiameter.value / 2.0 tr = topDiameter.value / 2.0 t = thickness.value s1 = Workplane("XY").circle(br).workplane(offset=height.value).circle(tr).loft() s2 = Workplane("XY").workplane(offset=bottomThickness.value).circle(br - t ).workplane(offset=height.value - t ).circle(tr - t).loft() cup = s1.cut(s2) cup.faces(">Z").edges().fillet(lipradius.value) return cup """ # for some reason shell doesnt work on this simple shape. how disappointing! td = 50.0 bd = 20.0 h = 10.0 t = 1.0 s1 = Workplane("XY").circle(bd).workplane(offset=h).circle(td).loft() s2 = ( Workplane("XY") .workplane(offset=t) .circle(bd - (2.0 * t)) .workplane(offset=(h - t)) .circle(td - (2.0 * t)) .loft() ) s3 = s1.cut(s2) self.saveModel(s3) def testEnclosure(self): """ Builds an electronics enclosure Original FreeCAD script: 81 source statements ,not including variables This script: 34 """ # parameter definitions p_outerWidth = 100.0 # Outer width of box enclosure p_outerLength = 150.0 # Outer length of box enclosure p_outerHeight = 50.0 # Outer height of box enclosure p_thickness = 3.0 # Thickness of the box walls p_sideRadius = 10.0 # Radius for the curves around the sides of the bo # Radius for the curves on the top and bottom edges of the box p_topAndBottomRadius = 2.0 # How far in from the edges the screwposts should be place. p_screwpostInset = 12.0 # nner Diameter of the screwpost holes, should be roughly screw diameter not including threads p_screwpostID = 4.0 # Outer Diameter of the screwposts.\nDetermines overall thickness of the posts p_screwpostOD = 10.0 p_boreDiameter = 8.0 # Diameter of the counterbore hole, if any p_boreDepth = 1.0 # Depth of the counterbore hole, if # Outer diameter of countersink. Should roughly match the outer diameter of the screw head p_countersinkDiameter = 0.0 # Countersink angle (complete angle between opposite sides, not from center to one side) p_countersinkAngle = 90.0 # Whether to place the lid with the top facing down or not. p_flipLid = True # Height of lip on the underside of the lid.\nSits inside the box body for a snug fit. p_lipHeight = 1.0 # outer shell oshell = ( Workplane("XY") .rect(p_outerWidth, p_outerLength) .extrude(p_outerHeight + p_lipHeight) ) # weird geometry happens if we make the fillets in the wrong order if p_sideRadius > p_topAndBottomRadius: oshell = ( oshell.edges("|Z") .fillet(p_sideRadius) .edges("#Z") .fillet(p_topAndBottomRadius) ) else: oshell = ( oshell.edges("#Z") .fillet(p_topAndBottomRadius) .edges("|Z") .fillet(p_sideRadius) ) # inner shell ishell = ( oshell.faces("Z") .workplane(-p_thickness) .rect(POSTWIDTH, POSTLENGTH, forConstruction=True) .vertices() .circle(p_screwpostOD / 2.0) .circle(p_screwpostID / 2.0) .extrude((-1.0) * (p_outerHeight + p_lipHeight - p_thickness), True) ) # split lid into top and bottom parts (lid, bottom) = ( box.faces(">Z") .workplane(-p_thickness - p_lipHeight) .split(keepTop=True, keepBottom=True) .all() ) # splits into two solids # translate the lid, and subtract the bottom from it to produce the lid inset lowerLid = lid.translate((0, 0, -p_lipHeight)) cutlip = lowerLid.cut(bottom).translate( (p_outerWidth + p_thickness, 0, p_thickness - p_outerHeight + p_lipHeight) ) # compute centers for counterbore/countersink or counterbore topOfLidCenters = ( cutlip.faces(">Z") .workplane() .rect(POSTWIDTH, POSTLENGTH, forConstruction=True) .vertices() ) # add holes of the desired type if p_boreDiameter > 0 and p_boreDepth > 0: topOfLid = topOfLidCenters.cboreHole( p_screwpostID, p_boreDiameter, p_boreDepth, (2.0) * p_thickness ) elif p_countersinkDiameter > 0 and p_countersinkAngle > 0: topOfLid = topOfLidCenters.cskHole( p_screwpostID, p_countersinkDiameter, p_countersinkAngle, (2.0) * p_thickness, ) else: topOfLid = topOfLidCenters.hole(p_screwpostID, (2.0) * p_thickness) # flip lid upside down if desired if p_flipLid: topOfLid.rotateAboutCenter((1, 0, 0), 180) # return the combined result result = topOfLid.union(bottom) self.saveModel(result) def testExtrude(self): """ Test extrude """ r = 1.0 h = 1.0 decimal_places = 9.0 # extrude in one direction s = Workplane("XY").circle(r).extrude(h, both=False) top_face = s.faces(">Z") bottom_face = s.faces("Z") bottom_face = s.faces("Z") bottom_face = s.faces("Z") bottom_face = s.faces(" 0) # cut a tapered hole s = ( Workplane("XY") .rect(2 * r, 2 * r) .extrude(2 * h) .faces(">Z") .workplane() .rect(r, r) .cutBlind(-h, taper=t) ) middle_face = s.faces(">Z[-2]") self.assertTrue(middle_face.val().Area() < 1) def testClose(self): # Close without endPoint and startPoint coincide. # Create a half-circle a = Workplane(Plane.XY()).sagittaArc((10, 0), 2).close().extrude(2) # Close when endPoint and startPoint coincide. # Create a double half-circle b = ( Workplane(Plane.XY()) .sagittaArc((10, 0), 2) .sagittaArc((0, 0), 2) .close() .extrude(2) ) # The b shape shall have twice the volume of the a shape. self.assertAlmostEqual(a.val().Volume() * 2.0, b.val().Volume()) # Testcase 3 from issue #238 thickness = 3.0 length = 10.0 width = 5.0 obj1 = ( Workplane("XY", origin=(0, 0, -thickness / 2)) .moveTo(length / 2, 0) .threePointArc((0, width / 2), (-length / 2, 0)) .threePointArc((0, -width / 2), (length / 2, 0)) .close() .extrude(thickness) ) os_x = 8.0 # Offset in X os_y = -19.5 # Offset in Y obj2 = ( Workplane("YZ", origin=(os_x, os_y, -thickness / 2)) .moveTo(os_x + length / 2, os_y) .sagittaArc((os_x - length / 2, os_y), width / 2) .sagittaArc((os_x + length / 2, os_y), width / 2) .close() .extrude(thickness) ) # The obj1 shape shall have the same volume as the obj2 shape. self.assertAlmostEqual(obj1.val().Volume(), obj2.val().Volume()) def testText(self): box = Workplane("XY").box(4, 4, 0.5) obj1 = ( box.faces(">Z") .workplane() .text( "CQ 2.0", 0.5, -0.05, cut=True, halign="left", valign="bottom", font="Sans", ) ) # combined object should have smaller volume self.assertGreater(box.val().Volume(), obj1.val().Volume()) obj2 = ( box.faces(">Z") .workplane() .text("CQ 2.0", 0.5, 0.05, cut=False, combine=True, font="Sans") ) # combined object should have bigger volume self.assertLess(box.val().Volume(), obj2.val().Volume()) # verify that the number of top faces is correct (NB: this is font specific) self.assertEqual(len(obj2.faces(">Z").vals()), 5) obj3 = ( box.faces(">Z") .workplane() .text( "CQ 2.0", 0.5, 0.05, cut=False, combine=False, halign="right", valign="top", font="Sans", ) ) # verify that the number of solids is correct self.assertEqual(len(obj3.solids().vals()), 5) def testParametricCurve(self): from math import sin, cos, pi k = 4 r = 1 func = lambda t: ( r * (k + 1) * cos(t) - r * cos((k + 1) * t), r * (k + 1) * sin(t) - r * sin((k + 1) * t), ) res_open = Workplane("XY").parametricCurve(func).extrude(3) # open profile generates an invalid solid self.assertFalse(res_open.solids().val().isValid()) res_closed = ( Workplane("XY").parametricCurve(func, start=0, stop=2 * pi).extrude(3) ) # closed profile will generate a valid solid with 3 faces self.assertTrue(res_closed.solids().val().isValid()) self.assertEqual(len(res_closed.faces().vals()), 3) def testMakeShellSolid(self): c0 = math.sqrt(2) / 4 vertices = [[c0, -c0, c0], [c0, c0, -c0], [-c0, c0, c0], [-c0, -c0, -c0]] faces_ixs = [[0, 1, 2, 0], [1, 0, 3, 1], [2, 3, 0, 2], [3, 2, 1, 3]] faces = [] for ixs in faces_ixs: lines = [] for v1, v2 in zip(ixs, ixs[1:]): lines.append( Edge.makeLine(Vector(*vertices[v1]), Vector(*vertices[v2])) ) wire = Wire.combine(lines) faces.append(Face.makeFromWires(wire)) shell = Shell.makeShell(faces) solid = Solid.makeSolid(shell) self.assertTrue(shell.isValid()) self.assertTrue(solid.isValid()) self.assertEqual(len(solid.Vertices()), 4) self.assertEqual(len(solid.Faces()), 4) def testIsInsideSolid(self): # test solid model = Workplane("XY").box(10, 10, 10) solid = model.val() # get first object on stack self.assertTrue(solid.isInside((0, 0, 0))) self.assertFalse(solid.isInside((10, 10, 10))) self.assertTrue(solid.isInside((Vector(3, 3, 3)))) self.assertFalse(solid.isInside((Vector(30.0, 30.0, 30.0)))) self.assertTrue(solid.isInside((0, 0, 4.99), tolerance=0.1)) self.assertTrue(solid.isInside((0, 0, 5))) # check point on surface self.assertTrue(solid.isInside((0, 0, 5.01), tolerance=0.1)) self.assertFalse(solid.isInside((0, 0, 5.1), tolerance=0.1)) # test compound solid model = Workplane("XY").box(10, 10, 10) model = model.moveTo(50, 50).box(10, 10, 10) solid = model.val() self.assertTrue(solid.isInside((0, 0, 0))) self.assertTrue(solid.isInside((50, 50, 0))) self.assertFalse(solid.isInside((50, 56, 0))) # make sure raises on non solid model = Workplane("XY").rect(10, 10) solid = model.val() with self.assertRaises(AttributeError): solid.isInside((0, 0, 0)) # test solid with an internal void void = Workplane("XY").box(10, 10, 10) model = Workplane("XY").box(100, 100, 100).cut(void) solid = model.val() self.assertFalse(solid.isInside((0, 0, 0))) self.assertTrue(solid.isInside((40, 40, 40))) self.assertFalse(solid.isInside((55, 55, 55))) def testWorkplaneCenterOptions(self): """ Test options for specifiying origin of workplane """ decimal_places = 9 pts = [(0, 0), (90, 0), (90, 30), (30, 30), (30, 60), (0.0, 60)] r = Workplane("XY").polyline(pts).close().extrude(10.0) origin = ( r.faces(">Z") .workplane(centerOption="ProjectedOrigin") .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) origin = ( r.faces(">Z").workplane(centerOption="CenterOfMass").plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (37.5, 22.5, 10.0), decimal_places) origin = ( r.faces(">Z") .workplane(centerOption="CenterOfBoundBox") .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (45.0, 30.0, 10.0), decimal_places) origin = ( r.faces(">Z") .workplane(centerOption="ProjectedOrigin", origin=(30, 10, 20)) .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) origin = ( r.faces(">Z") .workplane(centerOption="ProjectedOrigin", origin=Vector(30, 10, 20)) .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) with self.assertRaises(ValueError): origin = r.faces(">Z").workplane(centerOption="undefined") # test case where plane origin is shifted with center call r = ( r.faces(">Z") .workplane(centerOption="ProjectedOrigin") .center(30, 0) .hole(90) ) origin = ( r.faces(">Z") .workplane(centerOption="ProjectedOrigin") .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (30.0, 0.0, 10.0), decimal_places) origin = ( r.faces(">Z") .workplane(centerOption="ProjectedOrigin", origin=(0, 0, 0)) .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) # make sure projection works in all directions r = Workplane("YZ").polyline(pts).close().extrude(10.0) origin = ( r.faces(">X") .workplane(centerOption="ProjectedOrigin") .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (10.0, 0.0, 0.0), decimal_places) origin = ( r.faces(">X").workplane(centerOption="CenterOfMass").plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (10.0, 37.5, 22.5), decimal_places) origin = ( r.faces(">X") .workplane(centerOption="CenterOfBoundBox") .plane.origin.toTuple() ) self.assertTupleAlmostEquals(origin, (10.0, 45.0, 30.0), decimal_places) r = Workplane("XZ").polyline(pts).close().extrude(10.0) origin = ( r.faces("Z").workplane().slot2D(4, 1, 0).cutThruAll() self.assertAlmostEqual(result.val().Volume(), 21.214601837, decimal_places) result = box.faces(">Z").workplane().slot2D(4, 1, 0).cutBlind(-0.5) self.assertAlmostEqual(result.val().Volume(), 23.107300918, decimal_places) # Test to see if slot is rotated correctly result = Workplane("XY").slot2D(4, 1, 45).extrude(1) point = result.faces(">Z").edges(">X").first().val().startPoint().toTuple() self.assertTupleAlmostEquals( point, (0.707106781, 1.414213562, 1.0), decimal_places ) def test_assembleEdges(self): # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides # Passes an open wire to assembleEdges so that IsDone is true but Error returns 2 to test the warning functionality. edge_points = [ [-7.0, -7.0, 0.0], [-3.0, -10.0, 3.0], [7.0, -7.0, 0.0], [7.0, 7.0, 0.0], [-7.0, 7.0, 0.0], ] edge_wire = Workplane("XY").polyline( [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] ) edge_wire = edge_wire.add( Workplane("YZ") .workplane() .transformed(offset=Vector(0, 0, -7), rotate=Vector(0, 45, 0)) .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) ) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) # Embossed star, need to change optional parameters to obtain nice looking result. r1 = 3.0 r2 = 10.0 fn = 6 edge_points = [ [r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)] if i % 2 == 0 else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)] for i in range(2 * fn + 1) ] edge_wire = Workplane("XY").polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) # Points on hexagonal pattern coordinates, use of pushpoints. r1 = 1.0 fn = 6 edge_points = [ [r1 * math.cos(i * 2 * math.pi / fn), r1 * math.sin(i * 2 * math.pi / fn)] for i in range(fn + 1) ] surface_points = [ [0.25, 0, 0.75], [-0.25, 0, 0.75], [0, 0.25, 0.75], [0, -0.25, 0.75], [0, 0, 2], ] edge_wire = Workplane("XY").polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) # Gyroïd, all edges are splines on different workplanes. edge_points = [ [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], ] plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] edge_wire = ( Workplane(plane_list[0]) .workplane(offset=-offset_list[0]) .spline(edge_points[0]) ) for i in range(len(edge_points) - 1): edge_wire = edge_wire.add( Workplane(plane_list[i + 1]) .workplane(offset=-offset_list[i + 1]) .spline(edge_points[i + 1]) ) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) def testTag(self): # test tagging Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1, combine=False) result = (Workplane("XY").pushPoints([(-2, 0), (2, 0)]) .box(1, 1, 1, combine=False).tag("2 solids") .union(Workplane("XY").box(6, 1, 1))) self.assertEqual(len(result.objects), 1) result = result.getTagged("2 solids") self.assertEqual(len(result.objects), 2) # tags can be used to create geometry for construction part = ( # create a base solid Workplane("XY").box(1, 1, 1).tag("base") # do some stuff "for construction" .faces(">Y").workplane().box(1, 3, 1) .faces(">Z").workplane().box(2, 1, 1) .faces(">X").workplane() # create an edge, which CadQuery adds to the modelling context .circle(0.5) # go back to base object, but modelling context is preserved .getTagged("base") .faces(">Z").workplane().circle(0.5) # loft between the top of the base object and the wire at the end of the "for construction" code .loft() ) # assert face is made at the end of the construction geometry self.assertTupleAlmostEquals(part.faces(">X").val().Center().toTuple(), (1.0, 0.5, 1.5), 9) # assert face points in the x-direction, like the construction geometry self.assertTupleAlmostEquals(part.faces(">X").val().normalAt().toTuple(), (1.0, 0, 0), 9) def testCopyWorkplane(self): obj0 = Workplane("XY").box(1, 1, 10).faces(">Z").workplane() obj1 = Workplane("XY").copyWorkplane(obj0).box(1, 1, 1) self.assertTupleAlmostEquals((0, 0, 5), obj1.val().Center().toTuple(), 9) def testCopyWorkplaneFromTagged(self): # create a flat, wide base. Extrude one object 4 units high, another # object ontop of it 6 units high. Go back to base plane. Extrude an # object 11 units high. Assert that top face is 11 units high. result = (Workplane("XY").box(10, 10, 1, centered=(True, True, False)) .faces(">Z").workplane().tag("base").center(3, 0).rect(2, 2) .extrude(4).faces(">Z").workplane().circle(1).extrude(6) .copyWorkplaneFromTagged("base").center(-3, 0).circle(1) .extrude(11)) self.assertTupleAlmostEquals(result.faces(">Z").val().Center().toTuple(), (-3, 0, 12), 9) def testTagSelectors(self): result0 = Workplane("XY").box(1, 1, 1).tag("box").sphere(1) # result is currently a sphere self.assertEqual(1, result0.faces().size()) # a box has 8 vertices self.assertEqual(8, result0.vertices(tag="box").size()) # 6 faces self.assertEqual(6, result0.faces(tag="box").size()) # 12 edges self.assertEqual(12, result0.edges(tag="box").size()) # 6 wires self.assertEqual(6, result0.wires(tag="box").size()) # create two solids, tag them, join to one solid result1 = (Workplane("XY").pushPoints([(1, 0), (-1, 0)]).box(1, 1, 1) .tag("boxes").sphere(1)) self.assertEqual(1, result1.solids().size()) self.assertEqual(2, result1.solids(tag="boxes").size()) self.assertEqual(1, result1.shells().size()) self.assertEqual(2, result1.shells(tag="boxes").size()) # create 4 individual objects, tag it, then combine to one compound result2 = (Workplane("XY").rect(4, 4).vertices() .box(1, 1, 1, combine=False).tag("4 objs")) result2 = result2.newObject([Compound.makeCompound(result2.objects)]) self.assertEqual(1, result2.compounds().size()) self.assertEqual(0, result2.compounds(tag="4 objs").size())