Add Support for Specifying Arbitrary Tangents, etc. to spline()
* Added support for specifying the tangents at arbitrary interpolation points when interpolating a B-spline curve with `Workplane.spline()`. * Added support for specifying whether the tangents should be automatically scaled. (I.e., only use the tangent vector directions, rather than their magnitudes.) * Added support for specifying the value of the curve function parameter at the interpolation points. * Added support for specifying the interpolator's tolerance value. Q: _There are a number of whitespace, and other formatting changes introduced by `black`. Is there a specific list of parameters that you use when running code formatting?_
This commit is contained in:
@ -62,11 +62,11 @@ 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
|
||||
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
|
||||
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")
|
||||
|
||||
@ -89,8 +89,8 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def saveModel(self, shape):
|
||||
"""
|
||||
shape must be a CQ object
|
||||
Save models in SVG and STEP format
|
||||
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"))
|
||||
@ -155,11 +155,11 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testCylinderPlugin(self):
|
||||
"""
|
||||
Tests a cylinder plugin.
|
||||
The plugin creates cylinders of the specified radius and height for each item on the stack
|
||||
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
|
||||
This is a very short plugin that illustrates just about the simplest possible
|
||||
plugin
|
||||
"""
|
||||
|
||||
def cylinders(self, radius, height):
|
||||
@ -185,10 +185,10 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testPolygonPlugin(self):
|
||||
"""
|
||||
Tests a plugin to make regular polygons around points on the stack
|
||||
Tests a plugin to make regular polygons around points on the stack
|
||||
|
||||
Demonstratings using eachpoint to allow working in local coordinates
|
||||
to create geometry
|
||||
Demonstratings using eachpoint to allow working in local coordinates
|
||||
to create geometry
|
||||
"""
|
||||
|
||||
def rPoly(self, nSides, diameter):
|
||||
@ -565,12 +565,20 @@ class TestCadQuery(BaseTest):
|
||||
self.assertTrue(path_closed.IsClosed())
|
||||
|
||||
# attempt to build a valid face
|
||||
w = Wire.assembleEdges([path_closed,])
|
||||
w = Wire.assembleEdges(
|
||||
[
|
||||
path_closed,
|
||||
]
|
||||
)
|
||||
f = Face.makeFromWires(w)
|
||||
self.assertTrue(f.isValid())
|
||||
|
||||
# attempt to build an invalid face
|
||||
w = Wire.assembleEdges([path,])
|
||||
w = Wire.assembleEdges(
|
||||
[
|
||||
path,
|
||||
]
|
||||
)
|
||||
f = Face.makeFromWires(w)
|
||||
self.assertFalse(f.isValid())
|
||||
|
||||
@ -590,6 +598,118 @@ class TestCadQuery(BaseTest):
|
||||
path2 = Workplane("XY", (0, 0, 10)).spline(pts, tangents=tangents)
|
||||
self.assertAlmostEqual(path2.val().tangentAt(0).z, 0)
|
||||
|
||||
def testSplineWithMultipleTangents(self):
|
||||
"""
|
||||
Tests specifying B-spline tangents, besides the start point and end
|
||||
point tangents.
|
||||
"""
|
||||
|
||||
points = [(0, 0), (1, 1), (2, 0), (1, -1)]
|
||||
tangents = [(0, 1), (1, 0), (0, -1), (-1, 0)]
|
||||
parameters = range(len(points))
|
||||
|
||||
spline = (
|
||||
Workplane("XY")
|
||||
.spline(points, tangents=tangents, parameters=parameters)
|
||||
.consolidateWires()
|
||||
)
|
||||
|
||||
test_point = spline.edges().val().positionAt(2.5, mode="parameter")
|
||||
expected_test_point = Vector(1.875, -0.625, 0.0)
|
||||
|
||||
self.assertAlmostEqual((test_point - expected_test_point).Length, 0)
|
||||
|
||||
def testSplineWithSpecifiedAndUnspecifiedTangents(self):
|
||||
points = [(0, 0), (1, 1), (2, 0), (1, -1)]
|
||||
tangents = [(0, 1), None, (0, -1), (-1, 0)]
|
||||
parameters = range(len(points))
|
||||
|
||||
spline = (
|
||||
Workplane("XY")
|
||||
.spline(points, tangents=tangents, parameters=parameters)
|
||||
.consolidateWires()
|
||||
)
|
||||
|
||||
test_point = spline.edges().val().positionAt(1.5, mode="parameter")
|
||||
expected_test_point = Vector(1.6875, 0.875, 0.0)
|
||||
|
||||
self.assertAlmostEqual((test_point - expected_test_point).Length, 0)
|
||||
|
||||
def testSplineSpecifyingParameters(self):
|
||||
points = [(0, 0), (1, 1), (2, 0), (1, -1)]
|
||||
tangents = [(0, 1), (1, 0), (0, -1), (-1, 0)]
|
||||
|
||||
spline1 = (
|
||||
Workplane("XY")
|
||||
.spline(points, tangents=tangents, parameters=[0, 1, 2, 3])
|
||||
.consolidateWires()
|
||||
)
|
||||
# Multiply all parameter values by 10:
|
||||
spline2 = (
|
||||
Workplane("XY")
|
||||
.spline(points, tangents=tangents, parameters=[0, 10, 20, 30])
|
||||
.consolidateWires()
|
||||
)
|
||||
|
||||
# Test point equivalence for parameter, and pamater multiplied by 10:
|
||||
test_point1 = spline1.edges().val().positionAt(1.5, mode="parameter")
|
||||
test_point2 = spline2.edges().val().positionAt(15, mode="parameter")
|
||||
expected_test_point = Vector(1.625, 0.625, 0.0)
|
||||
|
||||
self.assertAlmostEqual((test_point1 - test_point2).Length, 0)
|
||||
self.assertAlmostEqual((test_point1 - expected_test_point).Length, 0)
|
||||
|
||||
def testSplineWithScaleTrue(self):
|
||||
points = [(0, 0), (1, 1), (2, 0), (1, -1)]
|
||||
tangents = [(0, 1), (1, 0), (0, -1), (-1, 0)]
|
||||
parameters = range(len(points))
|
||||
|
||||
spline = (
|
||||
Workplane("XY")
|
||||
.spline(points, tangents=tangents, parameters=parameters, scale=True)
|
||||
.consolidateWires()
|
||||
)
|
||||
|
||||
test_point = spline.edges().val().positionAt(0.5, mode="parameter")
|
||||
expected_test_point = Vector(0.375, 0.875, 0.0)
|
||||
|
||||
self.assertAlmostEqual((test_point - expected_test_point).Length, 0)
|
||||
|
||||
def testSplineTangentMagnitudeBelowToleranceThrows(self):
|
||||
import OCP
|
||||
|
||||
points = [(0, 0), (1, 1), (2, 0), (1, -1)]
|
||||
# Use a tangent vector with magnitude 0.5:
|
||||
tangents = [(0, 0.5), (1, 0), (0, -1), (-1, 0)]
|
||||
parameters = range(len(points))
|
||||
|
||||
# Set tolerance above the 0.5 length of the tangent vector. This
|
||||
# should throw an exception:
|
||||
with raises(
|
||||
(OCP.Standard.Standard_ConstructionError, OCP.Standard.Standard_Failure)
|
||||
):
|
||||
spline = (
|
||||
Workplane("XY")
|
||||
.spline(points, tangents=tangents, tolerance=1)
|
||||
.consolidateWires()
|
||||
)
|
||||
|
||||
def testSplineWithScaleFalse(self):
|
||||
points = [(0, 0), (1, 1), (2, 0), (1, -1)]
|
||||
tangents = [(0, 1), (1, 0), (0, -1), (-1, 0)]
|
||||
parameters = range(len(points))
|
||||
|
||||
spline = (
|
||||
Workplane("XY")
|
||||
.spline(points, tangents=tangents, parameters=parameters, scale=False)
|
||||
.consolidateWires()
|
||||
)
|
||||
|
||||
test_point = spline.edges().val().positionAt(0.5, mode="parameter")
|
||||
expected_test_point = Vector(0.375, 0.625, 0.0)
|
||||
|
||||
self.assertAlmostEqual((test_point - expected_test_point).Length, 0)
|
||||
|
||||
def testRotatedEllipse(self):
|
||||
def rotatePoint(x, y, alpha):
|
||||
# rotation matrix
|
||||
@ -777,7 +897,15 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testMakeEllipse(self):
|
||||
el = Wire.makeEllipse(
|
||||
1, 2, Vector(0, 0, 0), Vector(0, 0, 1), Vector(1, 0, 0), 0, 90, 45, True,
|
||||
1,
|
||||
2,
|
||||
Vector(0, 0, 0),
|
||||
Vector(0, 0, 1),
|
||||
Vector(1, 0, 0),
|
||||
0,
|
||||
90,
|
||||
45,
|
||||
True,
|
||||
)
|
||||
|
||||
self.assertTrue(el.IsClosed())
|
||||
@ -1267,7 +1395,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testSimpleWorkplane(self):
|
||||
"""
|
||||
A simple square part with a hole in it
|
||||
A simple square part with a hole in it
|
||||
"""
|
||||
s = Workplane(Plane.XY())
|
||||
r = (
|
||||
@ -1301,8 +1429,8 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
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
|
||||
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)
|
||||
@ -1312,8 +1440,8 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
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
|
||||
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 = (
|
||||
@ -1329,7 +1457,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testTwoWorkplanes(self):
|
||||
"""
|
||||
Tests a model that uses more than one workplane
|
||||
Tests a model that uses more than one workplane
|
||||
"""
|
||||
# base block
|
||||
s = Workplane(Plane.XY())
|
||||
@ -1453,7 +1581,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testCutThroughAll(self):
|
||||
"""
|
||||
Tests a model that uses more than one workplane
|
||||
Tests a model that uses more than one workplane
|
||||
"""
|
||||
# base block
|
||||
s = Workplane(Plane.XY())
|
||||
@ -1503,7 +1631,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testCutToFaceOffsetNOTIMPLEMENTEDYET(self):
|
||||
"""
|
||||
Tests cutting up to a given face, or an offset from a face
|
||||
Tests cutting up to a given face, or an offset from a face
|
||||
"""
|
||||
# base block
|
||||
s = Workplane(Plane.XY())
|
||||
@ -1729,7 +1857,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testSplineShape(self):
|
||||
"""
|
||||
Tests making a shape with an edge that is a spline
|
||||
Tests making a shape with an edge that is a spline
|
||||
"""
|
||||
s = Workplane(Plane.XY())
|
||||
sPnts = [
|
||||
@ -1747,7 +1875,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testSimpleMirror(self):
|
||||
"""
|
||||
Tests a simple mirroring operation
|
||||
Tests a simple mirroring operation
|
||||
"""
|
||||
s = (
|
||||
Workplane("XY")
|
||||
@ -1821,7 +1949,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testIbeam(self):
|
||||
"""
|
||||
Make an ibeam. demonstrates fancy mirroring
|
||||
Make an ibeam. demonstrates fancy mirroring
|
||||
"""
|
||||
s = Workplane(Plane.XY())
|
||||
L = 100.0
|
||||
@ -1920,7 +2048,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testCounterSinks(self):
|
||||
"""
|
||||
Tests countersinks
|
||||
Tests countersinks
|
||||
"""
|
||||
s = Workplane(Plane.XY())
|
||||
result = (
|
||||
@ -1993,7 +2121,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testSimpleShell(self):
|
||||
"""
|
||||
Create s simple box
|
||||
Create s simple box
|
||||
"""
|
||||
s1 = Workplane("XY").box(2, 2, 2).faces("+Z").shell(0.05)
|
||||
self.saveModel(s1)
|
||||
@ -2013,7 +2141,7 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testClosedShell(self):
|
||||
"""
|
||||
Create a hollow box
|
||||
Create a hollow box
|
||||
"""
|
||||
s1 = Workplane("XY").box(2, 2, 2).shell(-0.1)
|
||||
self.assertEqual(12, s1.faces().size())
|
||||
@ -2656,32 +2784,32 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testCup(self):
|
||||
"""
|
||||
UOM = "mm"
|
||||
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")
|
||||
#
|
||||
# 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()
|
||||
#
|
||||
# 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
|
||||
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!
|
||||
@ -2703,9 +2831,9 @@ class TestCadQuery(BaseTest):
|
||||
|
||||
def testEnclosure(self):
|
||||
"""
|
||||
Builds an electronics enclosure
|
||||
Original FreeCAD script: 81 source statements ,not including variables
|
||||
This script: 34
|
||||
Builds an electronics enclosure
|
||||
Original FreeCAD script: 81 source statements ,not including variables
|
||||
This script: 34
|
||||
"""
|
||||
|
||||
# parameter definitions
|
||||
|
Reference in New Issue
Block a user