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:
Pavel M. Penev
2021-02-14 05:21:46 -05:00
parent d1ebfbac22
commit 1caae595ed
3 changed files with 405 additions and 163 deletions

View File

@ -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