"""
    This module tests cadquery creation and manipulation functions
"""
# system modules
import math,os.path,time,tempfile
# 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., 3)
        self.assertAlmostEqual(o.y, 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 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.
        h = 1.
        decimal_places = 9.
        # 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,-.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,.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,.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("