Files
cadquery/tests/TestCadQuery.py
Michael Greminger bcf7141197 Fixed bug where telerance parameter of BoundingBox had of effect
The tolerance parameter of the BoundingBox method of shape had no effect. Fixed this by passing the tolerance to the _fromTopoDS call. Changed the tolerance default value from 0.1 to None so that the global TOL is used by default.  This allows the user to set the global TOL value as outlined in #74.  The CenterofBoundbox method incorrectly passed a shape to the BoundingBox method, which is the position for the tolerance paramter. This has been fixed.  The _fromTopoDS method hard coded the global variable TOL in it's call to BRepMesh_IncrementalMesh. This has been updated to use the user supplied tolerance if one has been provided. Added test coverage for the tolerance parameter of the BoundingBox method.
2019-07-29 10:20:55 -05:00

2053 lines
76 KiB
Python

"""
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 = """<html>
<head>
<style type="text/css">
.testResult{
background: #eeeeee;
margin: 50px;
border: 1px solid black;
}
</style>
</head>
<body>
<!--TEST_CONTENT-->
</body>
</html>"""
TEST_RESULT_TEMPLATE = """
<div class="testResult"><h3>%(name)s</h3>
%(svg)s
</div>
<!--TEST_CONTENT-->
"""
# 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(
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>', "")
# 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_CONTENT-->", 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("<Y").edges(
"<X").first().val().startPoint().toTuple()
endPoint = box.faces("<Y").edges(
"<X").first().val().endPoint().toTuple()
self.assertEqual(-0.5, startPoint[0])
self.assertEqual(-0.5, startPoint[1])
self.assertEqual(-2.5, startPoint[2])
self.assertEqual(-0.5, endPoint[0])
self.assertEqual(-0.5, endPoint[1])
self.assertEqual(2.5, endPoint[2])
def testLoft(self):
"""
Test making a lofted solid
:return:
"""
s = Workplane("XY").circle(4.0).workplane(5.0).rect(2.0, 2.0).loft()
self.saveModel(s)
# the result should have 7 faces
self.assertEqual(1, s.solids().size())
# the resulting loft had a split on the side, not sure why really, i expected only 3 faces
self.assertEqual(7, s.faces().size())
def testLoftCombine(self):
"""
test combining a lof with another feature
:return:
"""
s = Workplane("front").box(4.0, 4.0, 0.25).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").shell(-1.0 * t) # shell inwards not outwards
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("<Z").workplane(invert=True)
if lbumps > 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").vertices().item(0).val().Z, 3)
# TODO: it might be nice to provide a version of translate that modifies the existing geometry too
d = c.translate(Vector(0, 0, 1.5))
self.assertAlmostEqual(1.5, d.faces(
"<Z").vertices().item(0).val().Z, 3)
def testTranslateWire(self):
c = CQ(makeUnitSquareWire())
self.assertAlmostEqual(0.0, c.edges().vertices().item(0).val().Z, 3)
d = c.translate(Vector(0, 0, 1.5))
self.assertAlmostEqual(1.5, d.edges().vertices().item(0).val().Z, 3)
def testSolidReferencesCombine(self):
"test that solid references are preserved correctly"
c = CQ(makeUnitCube()) # the cube is the context solid
self.assertEqual(6, c.faces().size()) # cube has six faces
r = 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, True)\
.rect((p_outerWidth - 2.0 * p_thickness), (p_outerLength - 2.0 * p_thickness))\
.extrude((p_outerHeight - 2.0 * p_thickness), False) # set combine false to produce just the new boss
ishell = ishell.edges("|Z").fillet(p_sideRadius - p_thickness)
# make the box outer box
box = oshell.cut(ishell)
# make the screwposts
POSTWIDTH = (p_outerWidth - 2.0 * p_screwpostInset)
POSTLENGTH = (p_outerLength - 2.0 * p_screwpostInset)
box = box.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")
# calculate the distance between the top and the bottom face
delta = top_face.val().Center().sub(bottom_face.val().Center())
self.assertTupleAlmostEquals(delta.toTuple(),
(0., 0., h),
decimal_places)
# extrude symmetrically
s = Workplane("XY").circle(r).extrude(h, both=True)
top_face = s.faces(">Z")
bottom_face = s.faces("<Z")
# calculate the distance between the top and the bottom face
delta = top_face.val().Center().sub(bottom_face.val().Center())
self.assertTupleAlmostEquals(delta.toTuple(),
(0., 0., 2. * h),
decimal_places)
def testTaperedExtrudeCutBlind(self):
h = 1.
r = 1.
t = 5
# extrude with a positive taper
s = Workplane("XY").circle(r).extrude(h, taper=t)
top_face = s.faces(">Z")
bottom_face = s.faces("<Z")
# top and bottom face area
delta = top_face.val().Area() - bottom_face.val().Area()
self.assertTrue(delta < 0)
# extrude with a negative taper
s = Workplane("XY").circle(r).extrude(h, taper=-t)
top_face = s.faces(">Z")
bottom_face = s.faces("<Z")
# top and bottom face area
delta = top_face.val().Area() - bottom_face.val().Area()
self.assertTrue(delta > 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("<Y").workplane(centerOption='ProjectedOrigin') \
.plane.origin.toTuple()
self.assertTupleAlmostEquals(origin, (0.0, -10.0, 0.0), decimal_places)
origin = r.faces("<Y").workplane(centerOption='CenterOfMass') \
.plane.origin.toTuple()
self.assertTupleAlmostEquals(origin, (37.5, -10.0, 22.5), decimal_places)
origin = r.faces("<Y").workplane(centerOption='CenterOfBoundBox') \
.plane.origin.toTuple()
self.assertTupleAlmostEquals(origin, (45.0, -10.0, 30.0), decimal_places)
def testFindSolid(self):
r = Workplane("XY").pushPoints([(-2,0),(2,0)]).box(1,1,1,combine=False)
# there should be two solids on the stack
self.assertEqual(len(r.objects),2)
self.assertTrue(isinstance(r.val(),Solid))
# find solid should return a compund of two solids
s = r.findSolid()
self.assertEqual(len(s.Solids()),2)
self.assertTrue(isinstance(s,Compound))