Files
cadquery/tests/test_cadquery.py
Jeremy Wright ccf4ee20c6 Reworked the largestDimension logic based on bounding box extents.
The value returned now errs on the side of being very slightly larger, which is probably good considering this method's main use it to find the distance for thru cuts.
2020-04-14 21:01:13 -04:00

3396 lines
111 KiB
Python

"""
This module tests cadquery creation and manipulation functions
"""
# system modules
import math, os.path, time, tempfile
from random import choice
from random import random
from random import randrange
from pytest import approx
# 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 testPlaneRotateZNormal(self):
"""
Rotation of a plane in the Z direction should never alter its normal.
This test creates random planes. The plane is rotated a random angle in
the Z-direction to verify that the resulting plane maintains the same
normal.
The test also checks that the random origin is unaltered after
rotation.
"""
for _ in range(100):
angle = (random() - 0.5) * 720
xdir = Vector(random(), random(), random()).normalized()
rdir = Vector(random(), random(), random()).normalized()
zdir = xdir.cross(rdir).normalized()
origin = (random(), random(), random())
plane = Plane(origin=origin, xDir=xdir, normal=zdir)
rotated = plane.rotated((0, 0, angle))
assert rotated.zDir.toTuple() == approx(zdir.toTuple())
assert rotated.origin.toTuple() == approx(origin)
def testPlaneRotateConcat(self):
"""
Test the result of a well-known concatenated rotation example.
"""
xdir = (1, 0, 0)
normal = (0, 0, 1)
k = 2.0 ** 0.5 / 2.0
origin = (2, -1, 1)
plane = Plane(origin=origin, xDir=xdir, normal=normal)
plane = plane.rotated((0, 0, 45))
assert plane.xDir.toTuple() == approx((k, k, 0))
assert plane.yDir.toTuple() == approx((-k, k, 0))
assert plane.zDir.toTuple() == approx((0, 0, 1))
plane = plane.rotated((0, 45, 0))
assert plane.xDir.toTuple() == approx((0.5, 0.5, -k))
assert plane.yDir.toTuple() == approx((-k, k, 0))
assert plane.zDir.toTuple() == approx((0.5, 0.5, k))
assert plane.origin.toTuple() == origin
def testPlaneRotateConcatRandom(self):
"""
Rotation of a plane in a given direction should never alter that
direction.
This test creates a plane and rotates it a random angle in a given
direction. After the rotation, the direction of the resulting plane
in the rotation-direction should be constant.
The test also checks that the origin is unaltered after all rotations.
"""
origin = (2, -1, 1)
plane = Plane(origin=origin, xDir=(1, 0, 0), normal=(0, 0, 1))
for _ in range(100):
before = {
0: plane.xDir.toTuple(),
1: plane.yDir.toTuple(),
2: plane.zDir.toTuple(),
}
angle = (random() - 0.5) * 720
direction = randrange(3)
rotation = [0, 0, 0]
rotation[direction] = angle
plane = plane.rotated(rotation)
after = {
0: plane.xDir.toTuple(),
1: plane.yDir.toTuple(),
2: plane.zDir.toTuple(),
}
assert before[direction] == approx(after[direction])
assert plane.origin.toTuple() == origin
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 testLoftWithOneWireRaisesValueError(self):
s = Workplane("XY").circle(5)
with self.assertRaises(ValueError) as cm:
s.loft()
err = cm.exception
self.assertEqual(str(err), "More than one wire is required")
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())
# test tangents and offset plane
pts = [(0, 0), (-1, 1), (-2, 0), (-1, 0)]
tangents = [(0, 1), (1, 0)]
path2 = Workplane("XY", (0, 0, 10)).spline(pts, tangents=tangents)
self.assertAlmostEqual(path2.val().tangentAt(0).z, 0)
def testRotatedEllipse(self):
def rotatePoint(x, y, alpha):
# rotation matrix
a = alpha * DEG2RAD
r = ((math.cos(a), math.sin(a)), (-math.sin(a), math.cos(a)))
return ((x * r[0][0] + y * r[1][0]), (x * r[0][1] + y * r[1][1]))
def ellipsePoints(r1, r2, a):
return (r1 * math.cos(a * DEG2RAD), r2 * math.sin(a * DEG2RAD))
DEG2RAD = math.pi / 180.0
p0 = (10, 20)
a1, a2 = 30, -60
r1, r2 = 20, 10
ra = 25
sx_rot, sy_rot = rotatePoint(*ellipsePoints(r1, r2, a1), ra)
ex_rot, ey_rot = rotatePoint(*ellipsePoints(r1, r2, a2), ra)
# startAtCurrent=False, sense = 1
ellipseArc1 = (
Workplane("XY")
.moveTo(*p0)
.ellipseArc(
r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra
)
)
start = ellipseArc1.vertices().objects[0]
end = ellipseArc1.vertices().objects[1]
self.assertTupleAlmostEquals(
(start.X, start.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3
)
self.assertTupleAlmostEquals(
(end.X, end.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3
)
# startAtCurrent=True, sense = 1
ellipseArc2 = (
Workplane("XY")
.moveTo(*p0)
.ellipseArc(
r1, r2, startAtCurrent=True, angle1=a1, angle2=a2, rotation_angle=ra
)
)
start = ellipseArc2.vertices().objects[0]
end = ellipseArc2.vertices().objects[1]
self.assertTupleAlmostEquals(
(start.X, start.Y), (p0[0] + sx_rot - sx_rot, p0[1] + sy_rot - sy_rot), 3
)
self.assertTupleAlmostEquals(
(end.X, end.Y), (p0[0] + ex_rot - sx_rot, p0[1] + ey_rot - sy_rot), 3
)
# startAtCurrent=False, sense = -1
ellipseArc3 = (
Workplane("XY")
.moveTo(*p0)
.ellipseArc(
r1,
r2,
startAtCurrent=False,
angle1=a1,
angle2=a2,
rotation_angle=ra,
sense=-1,
)
)
start = ellipseArc3.vertices().objects[0]
end = ellipseArc3.vertices().objects[1]
# swap start and end points for coparison due to different sense
self.assertTupleAlmostEquals(
(start.X, start.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3
)
self.assertTupleAlmostEquals(
(end.X, end.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3
)
# startAtCurrent=True, sense = -1
ellipseArc4 = (
Workplane("XY")
.moveTo(*p0)
.ellipseArc(
r1,
r2,
startAtCurrent=True,
angle1=a1,
angle2=a2,
rotation_angle=ra,
sense=-1,
makeWire=True,
)
)
self.assertEqual(len(ellipseArc4.ctx.pendingWires), 1)
start = ellipseArc4.vertices().objects[0]
end = ellipseArc4.vertices().objects[1]
# swap start and end points for coparison due to different sense
self.assertTupleAlmostEquals(
(start.X, start.Y), (p0[0] + ex_rot - ex_rot, p0[1] + ey_rot - ey_rot), 3
)
self.assertTupleAlmostEquals(
(end.X, end.Y), (p0[0] + sx_rot - ex_rot, p0[1] + sy_rot - ey_rot), 3
)
def testEllipseArcsClockwise(self):
ellipseArc = (
Workplane("XY")
.moveTo(10, 15)
.ellipseArc(5, 4, -10, 190, 45, sense=-1, startAtCurrent=False)
)
sp = ellipseArc.val().startPoint()
ep = ellipseArc.val().endPoint()
self.assertTupleAlmostEquals(
(sp.x, sp.y), (7.009330014275797, 11.027027582524015), 3
)
self.assertTupleAlmostEquals(
(ep.x, ep.y), (13.972972417475985, 17.990669985724203), 3
)
ellipseArc = (
ellipseArc.ellipseArc(5, 4, -10, 190, 315, sense=-1)
.ellipseArc(5, 4, -10, 190, 225, sense=-1)
.ellipseArc(5, 4, -10, 190, 135, sense=-1)
)
ep = ellipseArc.val().endPoint()
self.assertTupleAlmostEquals((sp.x, sp.y), (ep.x, ep.y), 3)
def testEllipseArcsCounterClockwise(self):
ellipseArc = (
Workplane("XY")
.moveTo(10, 15)
.ellipseArc(5, 4, -10, 190, 45, startAtCurrent=False)
)
sp = ellipseArc.val().startPoint()
ep = ellipseArc.val().endPoint()
self.assertTupleAlmostEquals(
(sp.x, sp.y), (13.972972417475985, 17.990669985724203), 3
)
self.assertTupleAlmostEquals(
(ep.x, ep.y), (7.009330014275797, 11.027027582524015), 3
)
ellipseArc = (
ellipseArc.ellipseArc(5, 4, -10, 190, 135)
.ellipseArc(5, 4, -10, 190, 225)
.ellipseArc(5, 4, -10, 190, 315)
)
ep = ellipseArc.val().endPoint()
self.assertTupleAlmostEquals((sp.x, sp.y), (ep.x, ep.y), 3)
def testEllipseCenterAndMoveTo(self):
# Whether we start from a center() call or a moveTo call, it should be the same ellipse Arc
p0 = (10, 20)
a1, a2 = 30, -60
r1, r2 = 20, 10
ra = 25
ellipseArc1 = (
Workplane("XY")
.moveTo(*p0)
.ellipseArc(
r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra
)
)
sp1 = ellipseArc1.val().startPoint()
ep1 = ellipseArc1.val().endPoint()
ellipseArc2 = (
Workplane("XY")
.moveTo(*p0)
.ellipseArc(
r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra
)
)
sp2 = ellipseArc2.val().startPoint()
ep2 = ellipseArc2.val().endPoint()
self.assertTupleAlmostEquals(sp1.toTuple(), sp2.toTuple(), 3)
self.assertTupleAlmostEquals(ep1.toTuple(), ep2.toTuple(), 3)
def testMakeEllipse(self):
el = Wire.makeEllipse(
1, 2, Vector(0, 0, 0), Vector(0, 0, 1), Vector(1, 0, 0), 0, 90, 45, True,
)
self.assertTrue(el.IsClosed())
self.assertTrue(el.isValid())
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 testConcentricEllipses(self):
concentricEllipses = (
Workplane("XY").center(10, 20).ellipse(100, 10).center(0, 0).ellipse(50, 5)
)
v = concentricEllipses.vertices().objects[0]
self.assertTupleAlmostEquals((v.X, v.Y), (10 + 50, 20), 3)
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.0, 3)
self.assertAlmostEqual(o.y, 0.0, 3)
self.assertAlmostEqual(o.z, 0.5, 3)
def testTriangularPrism(self):
s = Workplane("XY").lineTo(1, 0).lineTo(1, 1).close().extrude(0.2)
self.saveModel(s)
def testMultiWireWorkplane(self):
"""
A simple square part with a hole in it-- but this time done as a single extrusion
with two wires, as opposed to s cut
"""
s = Workplane(Plane.XY())
r = s.rect(2.0, 2.0).circle(0.25).extrude(0.5)
self.saveModel(r)
self.assertEqual(7, r.faces().size())
def testConstructionWire(self):
"""
Tests a wire with several holes, that are based on the vertices of a square
also tests using a workplane plane other than XY
"""
s = Workplane(Plane.YZ())
r = (
s.rect(2.0, 2.0)
.rect(1.3, 1.3, forConstruction=True)
.vertices()
.circle(0.125)
.extrude(0.5)
)
self.saveModel(r)
# 10 faces-- 6 plus 4 holes, the vertices of the second rect.
self.assertEqual(10, r.faces().size())
def testTwoWorkplanes(self):
"""
Tests a model that uses more than one workplane
"""
# base block
s = Workplane(Plane.XY())
# TODO: this syntax is nice, but the iteration might not be worth
# the complexity.
# the simpler and slightly longer version would be:
# r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices()
# for c in r.all():
# c.circle(0.125).extrude(0.5,True)
r = (
s.rect(2.0, 2.0)
.rect(1.3, 1.3, forConstruction=True)
.vertices()
.circle(0.125)
.extrude(0.5)
)
# side hole, blind deep 1.9
t = r.faces(">Y").workplane().circle(0.125).cutBlind(-1.9)
self.saveModel(t)
self.assertEqual(12, t.faces().size())
def testCut(self):
"""
Tests the cut function by itself to catch the case where a Solid object is passed.
"""
s = Workplane(Plane.XY())
currentS = s.rect(2.0, 2.0).extrude(0.5)
toCut = s.rect(1.0, 1.0).extrude(0.5)
resS = currentS.cut(toCut.val())
self.assertEqual(10, resS.faces().size())
with self.assertRaises(ValueError):
currentS.cut(toCut.faces().val())
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)
b1 = Workplane("XY").box(1, 1, 1)
b2 = Workplane("XY", origin=(0, 0, 0.5)).box(1, 1, 1)
resS = b1.intersect(b2)
self.assertAlmostEqual(resS.val().Volume(), 0.5)
with self.assertRaises(ValueError):
b1.intersect(b2.faces().val())
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(1.76, dim, 1)
r = Workplane("XY").rect(1, 1).extrude(1)
dim = r.largestDimension()
self.assertAlmostEqual(1.76, 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))
c2 = c2.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25)
self.assertEqual(15, c2.faces().size())
def testCounterSinks(self):
"""
Tests countersinks
"""
s = Workplane(Plane.XY())
result = (
s.rect(2.0, 4.0)
.extrude(0.5)
.faces(">Z")
.workplane()
.rect(1.5, 3.5, forConstruction=True)
.vertices()
.cskHole(0.125, 0.25, 82, depth=None)
)
self.saveModel(result)
def testSplitKeepingHalf(self):
"""
Tests splitting a solid
"""
# drill a hole in the side
c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll()
self.assertEqual(7, c.faces().size())
# now cut it in half sideways
result = c.faces(">Y").workplane(-0.5).split(keepTop=True)
self.saveModel(result)
self.assertEqual(8, result.faces().size())
def testSplitKeepingBoth(self):
"""
Tests splitting a solid
"""
# drill a hole in the side
c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll()
self.assertEqual(7, c.faces().size())
# now cut it in half sideways
result = c.faces(">Y").workplane(-0.5).split(keepTop=True, keepBottom=True)
# stack will have both halves, original will be unchanged
# two solids are on the stack, eac
self.assertEqual(2, result.solids().size())
self.assertEqual(8, result.solids().item(0).faces().size())
self.assertEqual(8, result.solids().item(1).faces().size())
def testSplitKeepingBottom(self):
"""
Tests splitting a solid improperly
"""
# Drill a hole in the side
c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll()
self.assertEqual(7, c.faces().size())
# Now cut it in half sideways
result = c.faces(">Y").workplane(-0.5).split(keepTop=False, keepBottom=True)
# stack will have both halves, original will be unchanged
# one solid is on the stack
self.assertEqual(1, result.solids().size())
self.assertEqual(8, result.solids().item(0).faces().size())
def testBoxDefaults(self):
"""
Tests creating a single box
"""
s = Workplane("XY").box(2, 3, 4)
self.assertEqual(1, s.solids().size())
self.saveModel(s)
def testSimpleShell(self):
"""
Create s simple box
"""
s = Workplane("XY").box(2, 2, 2).faces("+Z").shell(0.05)
self.saveModel(s)
self.assertEqual(23, s.faces().size())
def testOpenCornerShell(self):
s = Workplane("XY").box(1, 1, 1)
s1 = s.faces("+Z")
s1.add(s.faces("+Y")).add(s.faces("+X"))
self.saveModel(s1.shell(0.2))
# Tests the list option variation of add
s1 = s.faces("+Z")
s1.add(s.faces("+Y")).add([s.faces("+X")])
# Tests the raw object option variation of add
s1 = s.faces("+Z")
s1.add(s.faces("+Y")).add(s.faces("+X").val().wrapped)
def testTopFaceFillet(self):
s = Workplane("XY").box(1, 1, 1).faces("+Z").edges().fillet(0.1)
self.assertEqual(s.faces().size(), 10)
self.saveModel(s)
def testBoxPointList(self):
"""
Tests creating an array of boxes
"""
s = (
Workplane("XY")
.rect(4.0, 4.0, forConstruction=True)
.vertices()
.box(0.25, 0.25, 0.25, combine=True)
)
# 1 object, 4 solids because the object is a compound
self.assertEqual(4, s.solids().size())
self.assertEqual(1, s.size())
self.saveModel(s)
s = (
Workplane("XY")
.rect(4.0, 4.0, forConstruction=True)
.vertices()
.box(0.25, 0.25, 0.25, combine=False)
)
# 4 objects, 4 solids, because each is a separate solid
self.assertEqual(4, s.size())
self.assertEqual(4, s.solids().size())
def testBoxCombine(self):
s = (
Workplane("XY")
.box(4, 4, 0.5)
.faces(">Z")
.workplane()
.rect(3, 3, forConstruction=True)
.vertices()
.box(0.25, 0.25, 0.25, combine=True)
)
self.saveModel(s)
self.assertEqual(1, s.solids().size()) # we should have one big solid
# should have 26 faces. 6 for the box, and 4x5 for the smaller cubes
self.assertEqual(26, s.faces().size())
def testSphereDefaults(self):
s = Workplane("XY").sphere(10)
self.saveModel(s) # Until FreeCAD fixes their sphere operation
self.assertEqual(1, s.solids().size())
self.assertEqual(1, s.faces().size())
def testSphereCustom(self):
s = Workplane("XY").sphere(
10, angle1=0, angle2=90, angle3=360, centered=(False, False, False)
)
self.saveModel(s)
self.assertEqual(1, s.solids().size())
self.assertEqual(2, s.faces().size())
def testSpherePointList(self):
s = (
Workplane("XY")
.rect(4.0, 4.0, forConstruction=True)
.vertices()
.sphere(0.25, combine=False)
)
# self.saveModel(s) # Until FreeCAD fixes their sphere operation
self.assertEqual(4, s.solids().size())
self.assertEqual(4, s.faces().size())
def testSphereCombine(self):
s = (
Workplane("XY")
.rect(4.0, 4.0, forConstruction=True)
.vertices()
.sphere(2.25, combine=True)
)
# self.saveModel(s) # Until FreeCAD fixes their sphere operation
self.assertEqual(1, s.solids().size())
self.assertEqual(4, s.faces().size())
def testWedgeDefaults(self):
s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5)
self.saveModel(s)
self.assertEqual(1, s.solids().size())
self.assertEqual(5, s.faces().size())
self.assertEqual(5, s.vertices().size())
def testWedgeCentering(self):
s = Workplane("XY").wedge(
10, 10, 10, 5, 5, 5, 5, centered=(False, False, False)
)
# self.saveModel(s)
self.assertEqual(1, s.solids().size())
self.assertEqual(5, s.faces().size())
self.assertEqual(5, s.vertices().size())
def testWedgePointList(self):
s = (
Workplane("XY")
.rect(4.0, 4.0, forConstruction=True)
.vertices()
.wedge(10, 10, 10, 5, 5, 5, 5, combine=False)
)
# self.saveModel(s)
self.assertEqual(4, s.solids().size())
self.assertEqual(20, s.faces().size())
self.assertEqual(20, s.vertices().size())
def testWedgeCombined(self):
s = (
Workplane("XY")
.rect(4.0, 4.0, forConstruction=True)
.vertices()
.wedge(10, 10, 10, 5, 5, 5, 5, combine=True)
)
# self.saveModel(s)
self.assertEqual(1, s.solids().size())
self.assertEqual(12, s.faces().size())
self.assertEqual(16, s.vertices().size())
def testQuickStartXY(self):
s = (
Workplane(Plane.XY())
.box(2, 4, 0.5)
.faces(">Z")
.workplane()
.rect(1.5, 3.5, forConstruction=True)
.vertices()
.cskHole(0.125, 0.25, 82, depth=None)
)
self.assertEqual(1, s.solids().size())
self.assertEqual(14, s.faces().size())
self.saveModel(s)
def testQuickStartYZ(self):
s = (
Workplane(Plane.YZ())
.box(2, 4, 0.5)
.faces(">X")
.workplane()
.rect(1.5, 3.5, forConstruction=True)
.vertices()
.cskHole(0.125, 0.25, 82, depth=None)
)
self.assertEqual(1, s.solids().size())
self.assertEqual(14, s.faces().size())
self.saveModel(s)
def testQuickStartXZ(self):
s = (
Workplane(Plane.XZ())
.box(2, 4, 0.5)
.faces(">Y")
.workplane()
.rect(1.5, 3.5, forConstruction=True)
.vertices()
.cskHole(0.125, 0.25, 82, depth=None)
)
self.assertEqual(1, s.solids().size())
self.assertEqual(14, s.faces().size())
self.saveModel(s)
def testDoubleTwistedLoft(self):
s = (
Workplane("XY")
.polygon(8, 20.0)
.workplane(offset=4.0)
.transformed(rotate=Vector(0, 0, 15.0))
.polygon(8, 20)
.loft()
)
s2 = (
Workplane("XY")
.polygon(8, 20.0)
.workplane(offset=-4.0)
.transformed(rotate=Vector(0, 0, 15.0))
.polygon(8, 20)
.loft()
)
# self.assertEquals(10,s.faces().size())
# self.assertEquals(1,s.solids().size())
s3 = s.combineSolids(s2)
self.saveModel(s3)
def testTwistedLoft(self):
s = (
Workplane("XY")
.polygon(8, 20.0)
.workplane(offset=4.0)
.transformed(rotate=Vector(0, 0, 15.0))
.polygon(8, 20)
.loft()
)
self.assertEqual(10, s.faces().size())
self.assertEqual(1, s.solids().size())
self.saveModel(s)
def testUnions(self):
# duplicates a memory problem of some kind reported when combining lots of objects
s = Workplane("XY").rect(0.5, 0.5).extrude(5.0)
o = []
beginTime = time.time()
for i in range(15):
t = Workplane("XY").center(10.0 * i, 0).rect(0.5, 0.5).extrude(5.0)
o.append(t)
# union stuff
for oo in o:
s = s.union(oo)
print("Total time %0.3f" % (time.time() - beginTime))
# Test unioning a Solid object
s = Workplane(Plane.XY())
currentS = s.rect(2.0, 2.0).extrude(0.5)
toUnion = s.rect(1.0, 1.0).extrude(1.0)
resS = currentS.union(toUnion)
self.assertEqual(11, resS.faces().size())
with self.assertRaises(ValueError):
resS.union(toUnion.faces().val())
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.0
h = 1.0
decimal_places = 9.0
# extrude in one direction
s = Workplane("XY").circle(r).extrude(h, both=False)
top_face = s.faces(">Z")
bottom_face = s.faces("<Z")
# 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, 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, 0.0, 2.0 * h), decimal_places
)
def testTaperedExtrudeCutBlind(self):
h = 1.0
r = 1.0
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,
-0.05,
cut=True,
halign="left",
valign="bottom",
font="Sans",
)
)
# combined object should have smaller volume
self.assertGreater(box.val().Volume(), obj1.val().Volume())
obj2 = (
box.faces(">Z")
.workplane()
.text("CQ 2.0", 0.5, 0.05, cut=False, combine=True, font="Sans")
)
# combined object should have bigger volume
self.assertLess(box.val().Volume(), obj2.val().Volume())
# verify that the number of top faces is correct (NB: this is font specific)
self.assertEqual(len(obj2.faces(">Z").vals()), 5)
obj3 = (
box.faces(">Z")
.workplane()
.text(
"CQ 2.0",
0.5,
0.05,
cut=False,
combine=False,
halign="right",
valign="top",
font="Sans",
)
)
# verify that the number of solids is correct
self.assertEqual(len(obj3.solids().vals()), 5)
def testParametricCurve(self):
from math import sin, cos, pi
k = 4
r = 1
func = lambda t: (
r * (k + 1) * cos(t) - r * cos((k + 1) * t),
r * (k + 1) * sin(t) - r * sin((k + 1) * t),
)
res_open = Workplane("XY").parametricCurve(func).extrude(3)
# open profile generates an invalid solid
self.assertFalse(res_open.solids().val().isValid())
res_closed = (
Workplane("XY").parametricCurve(func, start=0, stop=2 * pi).extrude(3)
)
# closed profile will generate a valid solid with 3 faces
self.assertTrue(res_closed.solids().val().isValid())
self.assertEqual(len(res_closed.faces().vals()), 3)
def testMakeShellSolid(self):
c0 = math.sqrt(2) / 4
vertices = [[c0, -c0, c0], [c0, c0, -c0], [-c0, c0, c0], [-c0, -c0, -c0]]
faces_ixs = [[0, 1, 2, 0], [1, 0, 3, 1], [2, 3, 0, 2], [3, 2, 1, 3]]
faces = []
for ixs in faces_ixs:
lines = []
for v1, v2 in zip(ixs, ixs[1:]):
lines.append(
Edge.makeLine(Vector(*vertices[v1]), Vector(*vertices[v2]))
)
wire = Wire.combine(lines)
faces.append(Face.makeFromWires(wire))
shell = Shell.makeShell(faces)
solid = Solid.makeSolid(shell)
self.assertTrue(shell.isValid())
self.assertTrue(solid.isValid())
self.assertEqual(len(solid.Vertices()), 4)
self.assertEqual(len(solid.Faces()), 4)
def testIsInsideSolid(self):
# test solid
model = Workplane("XY").box(10, 10, 10)
solid = model.val() # get first object on stack
self.assertTrue(solid.isInside((0, 0, 0)))
self.assertFalse(solid.isInside((10, 10, 10)))
self.assertTrue(solid.isInside((Vector(3, 3, 3))))
self.assertFalse(solid.isInside((Vector(30.0, 30.0, 30.0))))
self.assertTrue(solid.isInside((0, 0, 4.99), tolerance=0.1))
self.assertTrue(solid.isInside((0, 0, 5))) # check point on surface
self.assertTrue(solid.isInside((0, 0, 5.01), tolerance=0.1))
self.assertFalse(solid.isInside((0, 0, 5.1), tolerance=0.1))
# test compound solid
model = Workplane("XY").box(10, 10, 10)
model = model.moveTo(50, 50).box(10, 10, 10)
solid = model.val()
self.assertTrue(solid.isInside((0, 0, 0)))
self.assertTrue(solid.isInside((50, 50, 0)))
self.assertFalse(solid.isInside((50, 56, 0)))
# make sure raises on non solid
model = Workplane("XY").rect(10, 10)
solid = model.val()
with self.assertRaises(AttributeError):
solid.isInside((0, 0, 0))
# test solid with an internal void
void = Workplane("XY").box(10, 10, 10)
model = Workplane("XY").box(100, 100, 100).cut(void)
solid = model.val()
self.assertFalse(solid.isInside((0, 0, 0)))
self.assertTrue(solid.isInside((40, 40, 40)))
self.assertFalse(solid.isInside((55, 55, 55)))
def testWorkplaneCenterOptions(self):
"""
Test options for specifiying origin of workplane
"""
decimal_places = 9
pts = [(0, 0), (90, 0), (90, 30), (30, 30), (30, 60), (0.0, 60)]
r = Workplane("XY").polyline(pts).close().extrude(10.0)
origin = (
r.faces(">Z")
.workplane(centerOption="ProjectedOrigin")
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places)
origin = (
r.faces(">Z").workplane(centerOption="CenterOfMass").plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (37.5, 22.5, 10.0), decimal_places)
origin = (
r.faces(">Z")
.workplane(centerOption="CenterOfBoundBox")
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (45.0, 30.0, 10.0), decimal_places)
origin = (
r.faces(">Z")
.workplane(centerOption="ProjectedOrigin", origin=(30, 10, 20))
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places)
origin = (
r.faces(">Z")
.workplane(centerOption="ProjectedOrigin", origin=Vector(30, 10, 20))
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places)
with self.assertRaises(ValueError):
origin = r.faces(">Z").workplane(centerOption="undefined")
# test case where plane origin is shifted with center call
r = (
r.faces(">Z")
.workplane(centerOption="ProjectedOrigin")
.center(30, 0)
.hole(90)
)
origin = (
r.faces(">Z")
.workplane(centerOption="ProjectedOrigin")
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (30.0, 0.0, 10.0), decimal_places)
origin = (
r.faces(">Z")
.workplane(centerOption="ProjectedOrigin", origin=(0, 0, 0))
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places)
# make sure projection works in all directions
r = Workplane("YZ").polyline(pts).close().extrude(10.0)
origin = (
r.faces(">X")
.workplane(centerOption="ProjectedOrigin")
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (10.0, 0.0, 0.0), decimal_places)
origin = (
r.faces(">X").workplane(centerOption="CenterOfMass").plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (10.0, 37.5, 22.5), decimal_places)
origin = (
r.faces(">X")
.workplane(centerOption="CenterOfBoundBox")
.plane.origin.toTuple()
)
self.assertTupleAlmostEquals(origin, (10.0, 45.0, 30.0), decimal_places)
r = Workplane("XZ").polyline(pts).close().extrude(10.0)
origin = (
r.faces("<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))
def testSlot2D(self):
decimal_places = 9
# Ensure it produces a solid with the correct volume
result = Workplane("XY").slot2D(4, 1, 0).extrude(1)
self.assertAlmostEqual(result.val().Volume(), 3.785398163, decimal_places)
# Test for proper expected behaviour when cutting
box = Workplane("XY").box(5, 5, 1)
result = box.faces(">Z").workplane().slot2D(4, 1, 0).cutThruAll()
self.assertAlmostEqual(result.val().Volume(), 21.214601837, decimal_places)
result = box.faces(">Z").workplane().slot2D(4, 1, 0).cutBlind(-0.5)
self.assertAlmostEqual(result.val().Volume(), 23.107300918, decimal_places)
# Test to see if slot is rotated correctly
result = Workplane("XY").slot2D(4, 1, 45).extrude(1)
point = result.faces(">Z").edges(">X").first().val().startPoint().toTuple()
self.assertTupleAlmostEquals(
point, (0.707106781, 1.414213562, 1.0), decimal_places
)
def test_assembleEdges(self):
# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides
# Passes an open wire to assembleEdges so that IsDone is true but Error returns 2 to test the warning functionality.
edge_points = [
[-7.0, -7.0, 0.0],
[-3.0, -10.0, 3.0],
[7.0, -7.0, 0.0],
[7.0, 7.0, 0.0],
[-7.0, 7.0, 0.0],
]
edge_wire = Workplane("XY").polyline(
[(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)]
)
edge_wire = edge_wire.add(
Workplane("YZ")
.workplane()
.transformed(offset=Vector(0, 0, -7), rotate=Vector(0, 45, 0))
.spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)])
)
edge_wire = [o.vals()[0] for o in edge_wire.all()]
edge_wire = Wire.assembleEdges(edge_wire)
# Embossed star, need to change optional parameters to obtain nice looking result.
r1 = 3.0
r2 = 10.0
fn = 6
edge_points = [
[r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)]
if i % 2 == 0
else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)]
for i in range(2 * fn + 1)
]
edge_wire = Workplane("XY").polyline(edge_points)
edge_wire = [o.vals()[0] for o in edge_wire.all()]
edge_wire = Wire.assembleEdges(edge_wire)
# Points on hexagonal pattern coordinates, use of pushpoints.
r1 = 1.0
fn = 6
edge_points = [
[r1 * math.cos(i * 2 * math.pi / fn), r1 * math.sin(i * 2 * math.pi / fn)]
for i in range(fn + 1)
]
surface_points = [
[0.25, 0, 0.75],
[-0.25, 0, 0.75],
[0, 0.25, 0.75],
[0, -0.25, 0.75],
[0, 0, 2],
]
edge_wire = Workplane("XY").polyline(edge_points)
edge_wire = [o.vals()[0] for o in edge_wire.all()]
edge_wire = Wire.assembleEdges(edge_wire)
# Gyroïd, all edges are splines on different workplanes.
edge_points = [
[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]],
[[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]],
[[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]],
[[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]],
[[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]],
[[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]],
]
plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"]
offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54]
edge_wire = (
Workplane(plane_list[0])
.workplane(offset=-offset_list[0])
.spline(edge_points[0])
)
for i in range(len(edge_points) - 1):
edge_wire = edge_wire.add(
Workplane(plane_list[i + 1])
.workplane(offset=-offset_list[i + 1])
.spline(edge_points[i + 1])
)
edge_wire = [o.vals()[0] for o in edge_wire.all()]
edge_wire = Wire.assembleEdges(edge_wire)
def testTag(self):
# test tagging
result = (
Workplane("XY")
.pushPoints([(-2, 0), (2, 0)])
.box(1, 1, 1, combine=False)
.tag("2 solids")
.union(Workplane("XY").box(6, 1, 1))
)
self.assertEqual(len(result.objects), 1)
result = result._getTagged("2 solids")
self.assertEqual(len(result.objects), 2)
def testCopyWorkplane(self):
obj0 = Workplane("XY").box(1, 1, 10).faces(">Z").workplane()
obj1 = Workplane("XY").copyWorkplane(obj0).box(1, 1, 1)
self.assertTupleAlmostEquals((0, 0, 5), obj1.val().Center().toTuple(), 9)
def testWorkplaneFromTagged(self):
# create a flat, wide base. Extrude one object 4 units high, another
# object ontop of it 6 units high. Go back to base plane. Extrude an
# object 11 units high. Assert that top face is 11 units high.
result = (
Workplane("XY")
.box(10, 10, 1, centered=(True, True, False))
.faces(">Z")
.workplane()
.tag("base")
.center(3, 0)
.rect(2, 2)
.extrude(4)
.faces(">Z")
.workplane()
.circle(1)
.extrude(6)
.workplaneFromTagged("base")
.center(-3, 0)
.circle(1)
.extrude(11)
)
self.assertTupleAlmostEquals(
result.faces(">Z").val().Center().toTuple(), (-3, 0, 12), 9
)
def testTagSelectors(self):
result0 = Workplane("XY").box(1, 1, 1).tag("box").sphere(1)
# result is currently a sphere
self.assertEqual(1, result0.faces().size())
# a box has 8 vertices
self.assertEqual(8, result0.vertices(tag="box").size())
# 6 faces
self.assertEqual(6, result0.faces(tag="box").size())
# 12 edges
self.assertEqual(12, result0.edges(tag="box").size())
# 6 wires
self.assertEqual(6, result0.wires(tag="box").size())
# create two solids, tag them, join to one solid
result1 = (
Workplane("XY")
.pushPoints([(1, 0), (-1, 0)])
.box(1, 1, 1)
.tag("boxes")
.sphere(1)
)
self.assertEqual(1, result1.solids().size())
self.assertEqual(2, result1.solids(tag="boxes").size())
self.assertEqual(1, result1.shells().size())
self.assertEqual(2, result1.shells(tag="boxes").size())
# create 4 individual objects, tag it, then combine to one compound
result2 = (
Workplane("XY")
.rect(4, 4)
.vertices()
.box(1, 1, 1, combine=False)
.tag("4 objs")
)
result2 = result2.newObject([Compound.makeCompound(result2.objects)])
self.assertEqual(1, result2.compounds().size())
self.assertEqual(0, result2.compounds(tag="4 objs").size())
def test_interpPlate(self):
"""
Tests the interpPlate() functionnalites
Numerical values of Areas and Volumes were obtained with the Area() and Volume() functions on a Linux machine under Debian 10 with python 3.7.
"""
# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface.
thickness = 0
edge_points = [
[0.0, 0.0, 0.0],
[0.0, 10.0, 0.0],
[0.0, 10.0, 10.0],
[0.0, 0.0, 10.0],
]
surface_points = [[5.0, 5.0, 5.0]]
plate_0 = Workplane("XY").interpPlate(edge_points, surface_points, thickness)
self.assertTrue(plate_0.val().isValid())
self.assertAlmostEqual(plate_0.val().Area(), 141.218823892, 1)
# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides
thickness = 0.1
edge_points = [
[-7.0, -7.0, 0.0],
[-3.0, -10.0, 3.0],
[7.0, -7.0, 0.0],
[7.0, 7.0, 0.0],
[-7.0, 7.0, 0.0],
]
edge_wire = Workplane("XY").polyline(
[(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)]
)
# edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)]))
# In CadQuery Sept-2019 it worked with rotate=Vector(0, 45, 0). In CadQuery Dec-2019 rotate=Vector(45, 0, 0) only closes the wire.
edge_wire = edge_wire.add(
Workplane("YZ")
.workplane()
.transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0))
.spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)])
)
surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]]
plate_1 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
self.assertTrue(plate_1.val().isValid())
self.assertAlmostEqual(plate_1.val().Volume(), 26.124970206, 3)
# Embossed star, need to change optional parameters to obtain nice looking result.
r1 = 3.0
r2 = 10.0
fn = 6
thickness = 0.1
edge_points = [
[r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)]
if i % 2 == 0
else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)]
for i in range(2 * fn + 1)
]
edge_wire = Workplane("XY").polyline(edge_points)
r2 = 4.5
surface_points = [
[r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn), 1.0]
for i in range(2 * fn)
] + [[0.0, 0.0, -2.0]]
plate_2 = Workplane("XY").interpPlate(
edge_wire,
surface_points,
thickness,
combine=True,
clean=True,
degree=3,
nbPtsOnCur=15,
nbIter=2,
anisotropy=False,
tol2d=0.00001,
tol3d=0.0001,
tolAng=0.01,
tolCurv=0.1,
maxDeg=8,
maxSegments=49,
)
self.assertTrue(plate_2.val().isValid())
self.assertAlmostEqual(plate_2.val().Volume(), 10.956054314, 0)
# Points on hexagonal pattern coordinates, use of pushpoints.
r1 = 1.0
N = 3
ca = math.cos(30.0 * math.pi / 180.0)
sa = math.sin(30.0 * math.pi / 180.0)
# EVEN ROWS
pts = [
(-3.0, -3.0),
(-1.267949, -3.0),
(0.464102, -3.0),
(2.196152, -3.0),
(-3.0, 0.0),
(-1.267949, 0.0),
(0.464102, 0.0),
(2.196152, 0.0),
(-2.133974, -1.5),
(-0.401923, -1.5),
(1.330127, -1.5),
(3.062178, -1.5),
(-2.133975, 1.5),
(-0.401924, 1.5),
(1.330127, 1.5),
(3.062178, 1.5),
]
# Spike surface
thickness = 0.1
fn = 6
edge_points = [
[
r1 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180),
r1 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180),
]
for i in range(fn + 1)
]
surface_points = [
[
r1 / 4 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180),
r1 / 4 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180),
0.75,
]
for i in range(fn + 1)
] + [[0, 0, 2]]
edge_wire = Workplane("XY").polyline(edge_points)
plate_3 = (
Workplane("XY")
.pushPoints(pts)
.interpPlate(
edge_wire,
surface_points,
thickness,
combine=False,
clean=False,
degree=2,
nbPtsOnCur=20,
nbIter=2,
anisotropy=False,
tol2d=0.00001,
tol3d=0.0001,
tolAng=0.01,
tolCurv=0.1,
maxDeg=8,
maxSegments=9,
)
)
self.assertTrue(plate_3.val().isValid())
self.assertAlmostEqual(plate_3.val().Volume(), 0.45893954685189414, 1)
# Gyroïd, all edges are splines on different workplanes.
thickness = 0.1
edge_points = [
[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]],
[[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]],
[[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]],
[[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]],
[[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]],
[[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]],
]
plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"]
offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54]
edge_wire = (
Workplane(plane_list[0])
.workplane(offset=-offset_list[0])
.spline(edge_points[0])
)
for i in range(len(edge_points) - 1):
edge_wire = edge_wire.add(
Workplane(plane_list[i + 1])
.workplane(offset=-offset_list[i + 1])
.spline(edge_points[i + 1])
)
surface_points = [[0, 0, 0]]
plate_4 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness)
self.assertTrue(plate_4.val().isValid())
self.assertAlmostEqual(plate_4.val().Volume(), 7.760559490, 3)
def testTangentArcToPoint(self):
# create a simple shape with tangents of straight edges and see if it has the correct area
s0 = (
Workplane("XY")
.hLine(1)
.tangentArcPoint((1, 1), relative=False)
.hLineTo(0)
.tangentArcPoint((0, 0), relative=False)
.close()
.extrude(1)
)
area0 = s0.faces(">Z").val().Area()
self.assertAlmostEqual(area0, (1 + math.pi * 0.5 ** 2), 4)
# test relative coords
s1 = (
Workplane("XY")
.hLine(1)
.tangentArcPoint((0, 1), relative=True)
.hLineTo(0)
.tangentArcPoint((0, -1), relative=True)
.close()
.extrude(1)
)
self.assertTupleAlmostEquals(
s1.val().Center().toTuple(), s0.val().Center().toTuple(), 4
)
self.assertAlmostEqual(s1.val().Volume(), s0.val().Volume(), 4)
# consecutive tangent arcs
s1 = (
Workplane("XY")
.vLine(2)
.tangentArcPoint((1, 0))
.tangentArcPoint((1, 0))
.tangentArcPoint((1, 0))
.vLine(-2)
.close()
.extrude(1)
)
self.assertAlmostEqual(
s1.faces(">Z").val().Area(), 2 * 3 + 0.5 * math.pi * 0.5 ** 2, 4
)
# tangentArc on the end of a spline
# spline will be a simple arc of a circle, then finished off with a
# tangentArcPoint
angles = [idx * 1.5 * math.pi / 10 for idx in range(10)]
pts = [(math.sin(a), math.cos(a)) for a in angles]
s2 = (
Workplane("XY")
.spline(pts)
.tangentArcPoint((0, 1), relative=False)
.close()
.extrude(1)
)
# volume should almost be pi, but not accurately because we need to
# start with a spline
self.assertAlmostEqual(s2.val().Volume(), math.pi, 1)
# assert local coords are mapped to global correctly
arc0 = Workplane("XZ", origin=(1, 1, 1)).hLine(1).tangentArcPoint((1, 1)).val()
self.assertTupleAlmostEquals(arc0.endPoint().toTuple(), (3, 1, 2), 4)
# tangentArcPoint with 3-tuple argument
w0 = Workplane("XY").lineTo(1, 1).tangentArcPoint((1, 1, 1)).wire()
zmax = w0.val().BoundingBox().zmax
self.assertAlmostEqual(zmax, 1, 1)
def test_findFromEdge(self):
part = Workplane("XY", origin=(1, 1, 1)).hLine(1)
found_edge = part._findFromEdge(useLocalCoords=False)
self.assertTupleAlmostEquals(found_edge.startPoint().toTuple(), (1, 1, 1), 3)
self.assertTupleAlmostEquals(found_edge.Center().toTuple(), (1.5, 1, 1), 3)
self.assertTupleAlmostEquals(found_edge.endPoint().toTuple(), (2, 1, 1), 3)
found_edge = part._findFromEdge(useLocalCoords=True)
self.assertTupleAlmostEquals(found_edge.endPoint().toTuple(), (1, 0, 0), 3)
# check _findFromEdge can find a spline
pts = [(0, 0), (0, 1), (1, 2), (2, 4)]
spline0 = Workplane("XZ").spline(pts)._findFromEdge()
self.assertTupleAlmostEquals((2, 0, 4), spline0.endPoint().toTuple(), 3)
# check method fails if no edge is present
part2 = Workplane("XY").box(1, 1, 1)
with self.assertRaises(RuntimeError):
part2._findFromEdge()
with self.assertRaises(RuntimeError):
part2._findFromEdge(useLocalCoords=True)
def testMakeHelix(self):
h = 10
pitch = 1.5
r = 1.2
obj = Wire.makeHelix(pitch, h, r)
bb = obj.BoundingBox()
self.assertAlmostEqual(bb.zlen, h, 1)
def testUnionCompound(self):
box1 = Workplane("XY").box(10, 20, 30)
box2 = Workplane("YZ").box(10, 20, 30)
shape_to_cut = Workplane("XY").box(15, 15, 15).translate((8, 8, 8))
list_of_shapes = []
for o in box1.all():
list_of_shapes.extend(o.vals())
for o in box2.all():
list_of_shapes.extend(o.vals())
obj = Workplane("XY").newObject(list_of_shapes).cut(shape_to_cut)
assert obj.val().isValid()