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:
@ -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).
|
||||
"""
|
||||
|
||||
|
@ -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
|
||||
|
@ -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