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

@ -245,7 +245,7 @@ class Workplane(object):
# drill a hole in the side
c = Workplane().box(1,1,1).faces(">Z").workplane().circle(0.25).cutThruAll()
# now cut it in half sideways
c = c.faces(">Y").workplane(-0.5).split(keepTop=True)
"""
@ -327,7 +327,7 @@ class Workplane(object):
def size(self) -> int:
"""
Return the number of objects currently on the stack
Return the number of objects currently on the stack
"""
return len(self.objects)
@ -444,7 +444,7 @@ class Workplane(object):
* The centerOption paramter sets how the center is defined.
Options are 'CenterOfMass', 'CenterOfBoundBox', or 'ProjectedOrigin'.
'CenterOfMass' and 'CenterOfBoundBox' are in relation to the selected
face(s) or vertex (vertices). 'ProjectedOrigin' uses by default the current origin
face(s) or vertex (vertices). 'ProjectedOrigin' uses by default the current origin
or the optional origin parameter (if specified) and projects it onto the plane
defined by the selected face(s).
* The Z direction will be normal to the plane of the face,computed
@ -714,17 +714,17 @@ class Workplane(object):
tag: Optional[str] = None,
) -> "Workplane":
"""
Filters objects of the selected type with the specified selector,and returns results
Filters objects of the selected type with the specified selector,and returns results
:param objType: the type of object we are searching for
:type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid)
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object with the selected objects on the stack.
:param objType: the type of object we are searching for
:type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid)
:param tag: if set, search the tagged CQ object instead of self
:type tag: string
:return: a CQ object with the selected objects on the stack.
**Implementation Note**: This is the base implementation of the vertices,edges,faces,
solids,shells, and other similar selector methods. It is a useful extension point for
plugin developers to make other selector methods.
**Implementation Note**: This is the base implementation of the vertices,edges,faces,
solids,shells, and other similar selector methods. It is a useful extension point for
plugin developers to make other selector methods.
"""
cq_obj = self._getTagged(tag) if tag else self
# A single list of all faces from all objects on the stack
@ -1571,7 +1571,7 @@ class Workplane(object):
:param float distance: distance of the end of the line from the current point
:param float angle: angle of the vector to the end of the line with the x-axis
:return: the Workplane object with the current point at the end of the new line
"""
"""
x = math.cos(math.radians(angle)) * distance
y = math.sin(math.radians(angle)) * distance
@ -1672,6 +1672,9 @@ class Workplane(object):
listOfXYTuple: Iterable[VectorLike],
tangents: Optional[Sequence[VectorLike]] = None,
periodic: bool = False,
parameters: Optional[List[float]] = None,
scale: bool = True,
tolerance: Optional[float] = None,
forConstruction: bool = False,
includeCurrent: bool = False,
makeWire: bool = False,
@ -1681,8 +1684,33 @@ class Workplane(object):
:param listOfXYTuple: points to interpolate through
:type listOfXYTuple: list of 2-tuple
:param tangents: tuple of Vectors specifying start and finish tangent
:param tangents: vectors specifying the direction of the tangent to the
curve at each of the specified interpolation points.
If only 2 tangents are given, they will be used as the initial and
final tangent.
If some tangents are not specified (i.e., are None), no tangent
constraint will be applied to the corresponding interpolation point.
The spline will be C2 continuous at the interpolation points where
no tangent constraint is specified, and C1 continuous at the points
where a tangent constraint is specified.
:param periodic: creation of periodic curves
:param parameters: the value of the parameter at each interpolation point.
(The intepolated curve is represented as a vector-valued function of a
scalar parameter.)
If periodic == True, then len(parameters) must be
len(intepolation points) + 1, otherwise len(parameters) must be equal to
len(interpolation points).
:param scale: whether to scale the specified tangent vectors before
interpolating.
Each tangent is scaled, so it's length is equal to the derivative of
the Lagrange interpolated curve.
I.e., set this to True, if you want to use only the direction of
the tangent vectors specified by ``tangents``, but not their magnitude.
:param tolerance: tolerance of the algorithm (consult OCC documentation)
Used to check that the specified points are not too close to each
other, and that tangent vectors are not too short. (In either case
interpolation may fail.)
Set to None to use the default tolerance.
:param includeCurrent: use current point as a starting point of the curve
:param makeWire: convert the resulting spline edge to a wire
:return: a Workplane object with the current point at the end of the spline
@ -1721,15 +1749,32 @@ class Workplane(object):
allPoints = vecs
if tangents:
t1, t2 = Vector(tangents[0]), Vector(tangents[1])
tangents_g: Optional[Tuple[Vector, Vector]] = (
self.plane.toWorldCoords(t1) - self.plane.origin,
self.plane.toWorldCoords(t2) - self.plane.origin,
)
tangents_g: Optional[Sequence[Vector]] = [
self.plane.toWorldCoords(t) - self.plane.origin
if t is not None
else None
for t in tangents
]
else:
tangents_g = None
e = Edge.makeSpline(allPoints, tangents=tangents_g, periodic=periodic)
if tolerance is not None:
e = Edge.makeSpline(
allPoints,
tangents=tangents_g,
periodic=periodic,
parameters=parameters,
scale=scale,
tol=tolerance,
)
else:
e = Edge.makeSpline(
allPoints,
tangents=tangents_g,
periodic=periodic,
parameters=parameters,
scale=scale,
)
if makeWire:
rv_w = Wire.assembleEdges([e])
@ -2937,7 +2982,7 @@ class Workplane(object):
"""
Unions all of the items on the stack of toUnion with the current solid.
If there is no current solid, the items in toUnion are unioned together.
:param toUnion:
:type toUnion: a solid object, or a CQ object having a solid,
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape (default True)
@ -2999,7 +3044,7 @@ class Workplane(object):
) -> "Workplane":
"""
Cuts the provided solid from the current solid, IE, perform a solid subtraction
:param toCut: object to cut
:type toCut: a solid object, or a CQ object having a solid,
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
@ -3047,7 +3092,7 @@ class Workplane(object):
) -> "Workplane":
"""
Intersects the provided solid from the current solid.
:param toIntersect: object to intersect
:type toIntersect: a solid object, or a CQ object having a solid,
:param boolean clean: call :py:meth:`clean` afterwards to have a clean shape
@ -3711,7 +3756,7 @@ class Workplane(object):
def section(self, height: float = 0.0) -> "Workplane":
"""
Slices current solid at the given height.
:param float height: height to slice at (default: 0)
:return: a CQ object with the resulting face(s).
"""
@ -3732,7 +3777,7 @@ class Workplane(object):
def toPending(self) -> "Workplane":
"""
Adds wires/edges to pendingWires/pendingEdges.
:return: same CQ object with updated context.
"""
@ -3746,10 +3791,10 @@ class Workplane(object):
) -> "Workplane":
"""
Creates a 2D offset wire.
:param float d: thickness. Negative thickness denotes offset to inside.
:param kind: offset kind. Use "arc" for rounded and "intersection" for sharp edges (default: "arc")
:return: CQ object with resulting wire(s).
"""

View File

@ -34,8 +34,18 @@ from OCP.gp import (
gp_Elips,
)
# collection of points (used for spline construction)
# Array of points (used for B-spline construction):
from OCP.TColgp import TColgp_HArray1OfPnt
# Array of vectors (used for B-spline interpolation):
from OCP.TColgp import TColgp_Array1OfVec
# Array of booleans (used for B-spline interpolation):
from OCP.TColStd import TColStd_HArray1OfBoolean
# Array of floats (used for B-spline interpolation):
from OCP.TColStd import TColStd_HArray1OfReal
from OCP.BRepAdaptor import (
BRepAdaptor_Curve,
BRepAdaptor_CompCurve,
@ -1021,7 +1031,7 @@ class Vertex(Shape):
def __init__(self, obj: TopoDS_Shape, forConstruction: bool = False):
"""
Create a vertex from a FreeCAD Vertex
Create a vertex from a FreeCAD Vertex
"""
super(Vertex, self).__init__(obj)
@ -1035,7 +1045,7 @@ class Vertex(Shape):
def Center(self) -> Vector:
"""
The center of a vertex is itself!
The center of a vertex is itself!
"""
return Vector(self.toTuple())
@ -1061,7 +1071,9 @@ class Mixin1DProtocol(ShapeProtocol, Protocol):
...
def positionAt(
self, d: float, mode: Literal["length", "parameter"] = "length",
self,
d: float,
mode: Literal["length", "parameter"] = "length",
) -> Vector:
...
@ -1104,7 +1116,7 @@ class Mixin1D(object):
def paramAt(self: Mixin1DProtocol, d: float) -> float:
"""
Compute parameter value at the specified normalized distance.
:param d: normalized distance [0, 1]
:return: parameter value
"""
@ -1121,7 +1133,7 @@ class Mixin1D(object):
) -> Vector:
"""
Compute tangent vector at the specified location.
:param locationParam: distance or parameter value (default: 0.5)
:param mode: position calculation mode (default: parameter)
:return: tangent vector
@ -1144,7 +1156,7 @@ class Mixin1D(object):
def normal(self: Mixin1DProtocol) -> Vector:
"""
Calculate the normal Vector. Only possible for planar curves.
:return: normal vector
"""
@ -1321,9 +1333,6 @@ class Edge(Shape, Mixin1D):
angle1: float = 360.0,
angle2: float = 360,
) -> "Edge":
"""
"""
pnt = Vector(pnt)
dir = Vector(dir)
@ -1399,6 +1408,8 @@ class Edge(Shape, Mixin1D):
listOfVector: List[Vector],
tangents: Optional[Sequence[Vector]] = None,
periodic: bool = False,
parameters: Optional[List[float]] = None,
scale: bool = True,
tol: float = 1e-6,
) -> "Edge":
"""
@ -1407,19 +1418,60 @@ class Edge(Shape, Mixin1D):
:param listOfVector: a list of Vectors that represent the points
:param tangents: tuple of Vectors specifying start and finish tangent
:param periodic: creation of peridic curves
:param parameters: the value of the parameter at each interpolation point.
(The intepolated curve is represented as a vector-valued function of a
scalar parameter.)
If periodic == True, then len(parameters) must be
len(intepolation points) + 1, otherwise len(parameters) must be equal to
len(interpolation points).
:param scale: whether to scale the specified tangent vectors before
interpolating.
Each tangent is scaled, so it's length is equal to the derivative of
the Lagrange interpolated curve.
I.e., set this to True, if you want to use only the direction of
the tangent vectors specified by ``tangents``, but not their magnitude.
:param tol: tolerance of the algorithm (consult OCC documentation)
Used to check that the specified points are not too close to each
other, and that tangent vectors are not too short. (In either case
interpolation may fail.)
:return: an Edge
"""
pnts = TColgp_HArray1OfPnt(1, len(listOfVector))
for ix, v in enumerate(listOfVector):
pnts.SetValue(ix + 1, v.toPnt())
spline_builder = GeomAPI_Interpolate(pnts, periodic, tol)
if parameters is None:
spline_builder = GeomAPI_Interpolate(pnts, periodic, tol)
else:
parameters_array = TColStd_HArray1OfReal(1, len(parameters))
for index, value in enumerate(parameters):
parameters_array.SetValue(index + 1, value)
spline_builder = GeomAPI_Interpolate(pnts, parameters_array, periodic, tol)
if tangents:
v1, v2 = tangents
spline_builder.Load(v1.wrapped, v2.wrapped)
if len(tangents) == 2 and len(listOfVector) != 2:
# Specify only initial and final tangent:
t1, t2 = tangents
spline_builder.Load(t1.wrapped, t2.wrapped, scale)
else:
assert len(tangents) == len(listOfVector)
# Specify a tangent for each interpolation point:
tangents_array = TColgp_Array1OfVec(1, len(tangents))
tangent_enabled_array = TColStd_HArray1OfBoolean(1, len(tangents))
for index, value in enumerate(tangents):
tangent_enabled_array.SetValue(index + 1, value is not None)
tangent_vec = value if value is not None else Vector()
tangents_array.SetValue(index + 1, tangent_vec.wrapped)
spline_builder.Load(tangents_array, tangent_enabled_array, scale)
spline_builder.Perform()
if not spline_builder.IsDone():
raise ValueError(
"B-spline interpolation failed! Reason not reported by Open Cascade."
)
spline_geom = spline_builder.Curve()
return cls(BRepBuilderAPI_MakeEdge(spline_geom).Edge())
@ -1514,15 +1566,15 @@ class Wire(Shape, Mixin1D):
@classmethod
def assembleEdges(cls: Type["Wire"], listOfEdges: Iterable[Edge]) -> "Wire":
"""
Attempts to build a wire that consists of the edges in the provided list
:param cls:
:param listOfEdges: a list of Edge objects. The edges are not to be consecutive.
:return: a wire with the edges assembled
:BRepBuilderAPI_MakeWire::Error() values
:BRepBuilderAPI_WireDone = 0
:BRepBuilderAPI_EmptyWire = 1
:BRepBuilderAPI_DisconnectedWire = 2
:BRepBuilderAPI_NonManifoldWire = 3
Attempts to build a wire that consists of the edges in the provided list
:param cls:
:param listOfEdges: a list of Edge objects. The edges are not to be consecutive.
:return: a wire with the edges assembled
:BRepBuilderAPI_MakeWire::Error() values
:BRepBuilderAPI_WireDone = 0
:BRepBuilderAPI_EmptyWire = 1
:BRepBuilderAPI_DisconnectedWire = 2
:BRepBuilderAPI_NonManifoldWire = 3
"""
wire_builder = BRepBuilderAPI_MakeWire()
@ -1545,11 +1597,11 @@ class Wire(Shape, Mixin1D):
cls: Type["Wire"], radius: float, center: Vector, normal: Vector
) -> "Wire":
"""
Makes a Circle centered at the provided point, having normal in the provided direction
:param radius: floating point radius of the circle, must be > 0
:param center: vector representing the center of the circle
:param normal: vector representing the direction of the plane the circle should lie in
:return:
Makes a Circle centered at the provided point, having normal in the provided direction
:param radius: floating point radius of the circle, must be > 0
:param center: vector representing the center of the circle
:param normal: vector representing the direction of the plane the circle should lie in
:return:
"""
circle_edge = Edge.makeCircle(radius, center, normal)
@ -1570,15 +1622,15 @@ class Wire(Shape, Mixin1D):
closed: bool = True,
) -> "Wire":
"""
Makes an Ellipse centered at the provided point, having normal in the provided direction
:param x_radius: floating point major radius of the ellipse (x-axis), must be > 0
:param y_radius: floating point minor radius of the ellipse (y-axis), must be > 0
:param center: vector representing the center of the circle
:param normal: vector representing the direction of the plane the circle should lie in
:param angle1: start angle of arc
:param angle2: end angle of arc
:param rotation_angle: angle to rotate the created ellipse / arc
:return: Wire
Makes an Ellipse centered at the provided point, having normal in the provided direction
:param x_radius: floating point major radius of the ellipse (x-axis), must be > 0
:param y_radius: floating point minor radius of the ellipse (y-axis), must be > 0
:param center: vector representing the center of the circle
:param normal: vector representing the direction of the plane the circle should lie in
:param angle1: start angle of arc
:param angle2: end angle of arc
:param rotation_angle: angle to rotate the created ellipse / arc
:return: Wire
"""
ellipse_edge = Edge.makeEllipse(
@ -1715,11 +1767,11 @@ class Face(Shape):
def normalAt(self, locationVector: Optional[Vector] = None) -> Vector:
"""
Computes the normal vector at the desired location on the face.
Computes the normal vector at the desired location on the face.
:returns: a vector representing the direction
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
:type locationVector: a vector that lies on the surface.
:returns: a vector representing the direction
:param locationVector: the location to compute the normal at. If none, the center of the face is used.
:type locationVector: a vector that lies on the surface.
"""
# get the geometry
surface = self._geomAdaptor()
@ -1779,7 +1831,7 @@ class Face(Shape):
:param points
:type points: list of gp_Pnt
:param edges
:type edges: list of Edge
:type edges: list of Edge
:param continuity=GeomAbs_C0
:type continuity: OCC.Core.GeomAbs continuity condition
:param Degree = 3 (OCCT default)
@ -2165,7 +2217,7 @@ class Solid(Shape, Mixin3D):
@staticmethod
def isSolid(obj: Shape) -> bool:
"""
Returns true if the object is a solid, false otherwise
Returns true if the object is a solid, false otherwise
"""
if hasattr(obj, "ShapeType"):
if obj.ShapeType == "Solid" or (
@ -2274,9 +2326,9 @@ class Solid(Shape, Mixin3D):
cls: Type["Solid"], listOfWire: List[Wire], ruled: bool = False
) -> "Solid":
"""
makes a loft from a list of wires
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
wants to make an infinitely thin shell for a real FreeCADPart.
makes a loft from a list of wires
The wires will be converted into faces when possible-- it is presumed that nobody ever actually
wants to make an infinitely thin shell for a real FreeCADPart.
"""
# the True flag requests building a solid instead of a shell.
if len(listOfWire) < 2:
@ -2362,28 +2414,32 @@ class Solid(Shape, Mixin3D):
angleDegrees: float,
) -> "Solid":
"""
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector.
Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
construction methods used here are different enough that they should be separate.
Though the signature may appear to be similar enough to extrudeLinear to merit combining them, the
construction methods used here are different enough that they should be separate.
At a high level, the steps followed are:
(1) accept a set of wires
(2) create another set of wires like this one, but which are transformed and rotated
(3) create a ruledSurface between the sets of wires
(4) create a shell and compute the resulting object
At a high level, the steps followed are:
(1) accept a set of wires
(2) create another set of wires like this one, but which are transformed and rotated
(3) create a ruledSurface between the sets of wires
(4) create a shell and compute the resulting object
:param outerWire: the outermost wire, a cad.Wire
:param innerWires: a list of inner wires, a list of cad.Wire
:param vecCenter: the center point about which to rotate. the axis of rotation is defined by
vecNormal, located at vecCenter. ( a cad.Vector )
:param vecNormal: a vector along which to extrude the wires ( a cad.Vector )
:param angleDegrees: the angle to rotate through while extruding
:return: a cad.Solid object
:param outerWire: the outermost wire, a cad.Wire
:param innerWires: a list of inner wires, a list of cad.Wire
:param vecCenter: the center point about which to rotate. the axis of rotation is defined by
vecNormal, located at vecCenter. ( a cad.Vector )
:param vecNormal: a vector along which to extrude the wires ( a cad.Vector )
:param angleDegrees: the angle to rotate through while extruding
:return: a cad.Solid object
"""
# make straight spine
straight_spine_e = Edge.makeLine(vecCenter, vecCenter.add(vecNormal))
straight_spine_w = Wire.combine([straight_spine_e,])[0].wrapped
straight_spine_w = Wire.combine(
[
straight_spine_e,
]
)[0].wrapped
# make an auxliliary spine
pitch = 360.0 / angleDegrees * vecNormal.Length
@ -2418,26 +2474,26 @@ class Solid(Shape, Mixin3D):
taper: float = 0,
) -> "Solid":
"""
Attempt to extrude the list of wires into a prismatic solid in the provided direction
Attempt to extrude the list of wires into a prismatic solid in the provided direction
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param vecNormal: a vector along which to extrude the wires
:param taper: taper angle, default=0
:return: a Solid object
:param outerWire: the outermost wire
:param innerWires: a list of inner wires
:param vecNormal: a vector along which to extrude the wires
:param taper: taper angle, default=0
:return: a Solid object
The wires must not intersect
The wires must not intersect
Extruding wires is very non-trivial. Nested wires imply very different geometry, and
there are many geometries that are invalid. In general, the following conditions must be met:
Extruding wires is very non-trivial. Nested wires imply very different geometry, and
there are many geometries that are invalid. In general, the following conditions must be met:
* all wires must be closed
* there cannot be any intersecting or self-intersecting wires
* wires must be listed from outside in
* more than one levels of nesting is not supported reliably
* all wires must be closed
* there cannot be any intersecting or self-intersecting wires
* wires must be listed from outside in
* more than one levels of nesting is not supported reliably
This method will attempt to sort the wires, but there is much work remaining to make this method
reliable.
This method will attempt to sort the wires, but there is much work remaining to make this method
reliable.
"""
if taper == 0:
@ -2530,7 +2586,11 @@ class Solid(Shape, Mixin3D):
def _toWire(p: Union[Edge, Wire]) -> Wire:
if isinstance(p, Edge):
rv = Wire.assembleEdges([p,])
rv = Wire.assembleEdges(
[
p,
]
)
else:
rv = p
@ -2611,7 +2671,11 @@ class Solid(Shape, Mixin3D):
:return: a Solid object
"""
if isinstance(path, Edge):
w = Wire.assembleEdges([path,]).wrapped
w = Wire.assembleEdges(
[
path,
]
).wrapped
else:
w = path.wrapped
@ -2768,7 +2832,7 @@ class Compound(Shape, Mixin3D):
def __iter__(self) -> Iterator[Shape]:
"""
Iterate over subshapes.
Iterate over subshapes.
"""
@ -2850,6 +2914,11 @@ def sortWiresByBuildOrder(wireList: List[Wire]) -> List[List[Wire]]:
rv = []
for face in faces.Faces():
rv.append([face.outerWire(),] + face.innerWires())
rv.append(
[
face.outerWire(),
]
+ face.innerWires()
)
return rv

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