def profile_to_cross_section(profile, lefthand=False, start_count=1, min_vertices=20):
    r"""
    Converts a thread profile to it's equivalent cross-section.

    **Profile:**

    The thread profile contains a single wire along the XZ plane
    (note: wire will be projected onto the XZ plane; Y-coords will be ignored).
    The profile is expected to be of 1 thread rotation, so it's height
    (along the Z-axis) is the thread's "pitch".
    If start_count > 1, then the profile will effectively be duplicated.
    The resulting cross-section is designed to be swept along a helical path
    with a pitch of the thread's "lead" (which is {the height of the given
    profile} * start_count)

    **Method:**

    Each edge of the profile is converted to a bezier spline, aproximating
    its polar plot equivalent.

    **Resolution:** (via `min_vertices` parameter)

    Increasing the number of vertices used to define the bezier will
    increase the resulting thread's accuracy, but cost more to render.

    min_vertices may also be expressed as a list to set the number of
    vertices to set for each wire.
    where: len(min_vertices) == number of edges in profile

    **Example**

    .. doctest::

        import cadquery
        from cqparts_fasteners.solidtypes.threads.base import profile_to_cross_section
        from Helpers import show  # doctest: +SKIP

        profile = cadquery.Workplane("XZ") \
            .moveTo(1, 0) \
            .lineTo(2, 1).lineTo(1, 2) \
            .wire()
        cross_section = profile_to_cross_section(profile)

        show(profile)  # doctest: +SKIP
        show(cross_section)  # doctest: +SKIP

    Will result in:

    .. image:: /_static/img/solidtypes.threads.base.profile_to_cross_section.01.png

    :param profile: workplane containing wire of thread profile.
    :type profile: :class:`cadquery.Workplane`
    :param lefthand: if True, cross-section is made backwards.
    :type lefthand: :class:`bool`
    :param start_count: profile is duplicated this many times.
    :type start_count: :class:`int`
    :param min_vertices: int or tuple of the desired resolution.
    :type min_vertices: :class:`int` or :class:`tuple`
    :return: workplane with a face ready to be swept into a thread.
    :rtype: :class:`cadquery.Workplane`
    :raises TypeError: if a problem is found with the given parameters.
    :raises ValueError: if ``min_vertices`` is a list with elements not equal to the numbmer of wire edges.
    """
    # verify parameter(s)
    if not isinstance(profile, cadquery.Workplane):
        raise TypeError("profile %r must be a %s instance" % (profile, cadquery.Workplane))
    if not isinstance(min_vertices, (int, list, tuple)):
        raise TypeError("min_vertices %r must be an int, list, or tuple" % (min_vertices))

    # get wire from Workplane
    wire = profile.val()  # cadquery.Wire
    if not isinstance(wire, cadquery.Wire):
        raise TypeError("a valid profile Wire type could not be found in the given Workplane")

    profile_bb = wire.BoundingBox()
    pitch = profile_bb.zmax - profile_bb.zmin
    lead = pitch * start_count

    # determine vertices count per edge
    edges = wire.Edges()
    vertices_count = None
    if isinstance(min_vertices, int):
        # evenly spread vertices count along profile wire
        # (weighted by the edge's length)
        vertices_count = [
            int(ceil(round(e.Length() / wire.Length(), 7) * min_vertices))
            for e in edges
        ]
        # rounded for desired contrived results
        # (trade-off: an error of 1 is of no great consequence)
    else:
        # min_vertices is defined per edge (already what we want)
        if len(min_vertices) != len(edges):
            raise ValueError(
                "min_vertices list size does not match number of profile edges: "
                "len(%r) != %i" % (min_vertices, len(edges))
            )
        vertices_count = min_vertices

    # Utilities for building cross-section
    def get_xz(vertex):
        if isinstance(vertex, cadquery.Vector):
            vertex = vertex.wrapped  # TODO: remove this, it's messy
        # where isinstance(vertex, FreeCAD.Base.Vector)
        return (vertex.x, vertex.z)

    def cart2polar(x, z, z_offset=0):
        """
        Convert cartesian coordinates to polar coordinates.
        Uses thread's lead height to give full 360deg translation.
        """
        radius = x
        angle = ((z + z_offset) / lead) * (2 * pi)  # radians
        if not lefthand:
            angle = -angle
        return (radius, angle)

    def transform(vertex, z_offset=0):
        # where isinstance(vertex, FreeCAD.Base.Vector)
        """
        Transform profile vertex on the XZ plane to it's equivalent on
        the cross-section's XY plane
        """
        (radius, angle) = cart2polar(*get_xz(vertex), z_offset=z_offset)
        return (radius * cos(angle), radius * sin(angle))

    # Conversion methods
    def apply_spline(wp, edge, vert_count, z_offset=0):
        """
        Trace along edge and create a spline from the transformed verteces.
        """
        curve = edge.wrapped.Curve  # FreeCADPart.Geom* (depending on type)
        if edge.geomType() == 'CIRCLE':
            iter_dist = edge.wrapped.ParameterRange[1] / vert_count
        else:
            iter_dist = edge.Length() / vert_count
        points = []
        for j in range(vert_count):
            dist = (j + 1) * iter_dist
            vert = curve.value(dist)
            points.append(transform(vert, z_offset))
        return wp.spline(points)

    def apply_arc(wp, edge, z_offset=0):
        """
        Create an arc using edge's midpoint and endpoint.
        Only intended for use for vertical lines on the given profile.
        """
        return wp.threePointArc(
            point1=transform(edge.wrapped.valueAt(edge.Length() / 2), z_offset),
            point2=transform(edge.wrapped.valueAt(edge.Length()), z_offset),
        )

    def apply_radial_line(wp, edge, z_offset=0):
        """
        Create a straight radial line
        """
        return wp.lineTo(*transform(edge.endPoint(), z_offset))

    # Build cross-section
    start_v = edges[0].startPoint().wrapped
    cross_section = cadquery.Workplane("XY") \
        .moveTo(*transform(start_v))

    for i in range(start_count):
        z_offset = i * pitch
        for (j, edge) in enumerate(wire.Edges()):
            # where: isinstance(edge, cadquery.Edge)
            if (edge.geomType() == 'LINE') and (edge.startPoint().x == edge.endPoint().x):
                # edge is a vertical line, plot a circular arc
                cross_section = apply_arc(cross_section, edge, z_offset)
            elif (edge.geomType() == 'LINE') and (edge.startPoint().z == edge.endPoint().z):
                # edge is a horizontal line, plot a radial line
                cross_section = apply_radial_line(cross_section, edge, z_offset)
            else:
                # create bezier spline along transformed points (default)
                cross_section = apply_spline(cross_section, edge, vertices_count[j], z_offset)

    return cross_section.close()
def profile_to_cross_section ( profile , lefthand = False , start_count = 1 , min_vertices = 20 ) : if not isinstance ( profile , cadquery . Workplane ) : raise TypeError ( "profile %r must be a %s instance" % ( profile , cadquery . Workplane ) ) if not isinstance ( min_vertices , ( int , list , tuple ) ) : raise TypeError ( "min_vertices %r must be an int, list, or tuple" % ( min_vertices ) ) wire = profile . val ( ) if not isinstance ( wire , cadquery . Wire ) : raise TypeError ( "a valid profile Wire type could not be found in the given Workplane" ) profile_bb = wire . BoundingBox ( ) pitch = profile_bb . zmax - profile_bb . zmin lead = pitch * start_count edges = wire . Edges ( ) vertices_count = None if isinstance ( min_vertices , int ) : vertices_count = [ int ( ceil ( round ( e . Length ( ) / wire . Length ( ) , 7 ) * min_vertices ) ) for e in edges ] else : if len ( min_vertices ) != len ( edges ) : raise ValueError ( "min_vertices list size does not match number of profile edges: " "len(%r) != %i" % ( min_vertices , len ( edges ) ) ) vertices_count = min_vertices def get_xz ( vertex ) : if isinstance ( vertex , cadquery . Vector ) : vertex = vertex . wrapped return ( vertex . x , vertex . z ) def cart2polar ( x , z , z_offset = 0 ) : radius = x angle = ( ( z + z_offset ) / lead ) * ( 2 * pi ) if not lefthand : angle = - angle return ( radius , angle ) def transform ( vertex , z_offset = 0 ) : ( radius , angle ) = cart2polar ( * get_xz ( vertex ) , z_offset = z_offset ) return ( radius * cos ( angle ) , radius * sin ( angle ) ) def apply_spline ( wp , edge , vert_count , z_offset = 0 ) : curve = edge . wrapped . Curve if edge . geomType ( ) == 'CIRCLE' : iter_dist = edge . wrapped . ParameterRange [ 1 ] / vert_count else : iter_dist = edge . Length ( ) / vert_count points = [ ] for j in range ( vert_count ) : dist = ( j + 1 ) * iter_dist vert = curve . value ( dist ) points . append ( transform ( vert , z_offset ) ) return wp . spline ( points ) def apply_arc ( wp , edge , z_offset = 0 ) : return wp . threePointArc ( point1 = transform ( edge . wrapped . valueAt ( edge . Length ( ) / 2 ) , z_offset ) , point2 = transform ( edge . wrapped . valueAt ( edge . Length ( ) ) , z_offset ) , ) def apply_radial_line ( wp , edge , z_offset = 0 ) : return wp . lineTo ( * transform ( edge . endPoint ( ) , z_offset ) ) start_v = edges [ 0 ] . startPoint ( ) . wrapped cross_section = cadquery . Workplane ( "XY" ) . moveTo ( * transform ( start_v ) ) for i in range ( start_count ) : z_offset = i * pitch for ( j , edge ) in enumerate ( wire . Edges ( ) ) : if ( edge . geomType ( ) == 'LINE' ) and ( edge . startPoint ( ) . x == edge . endPoint ( ) . x ) : cross_section = apply_arc ( cross_section , edge , z_offset ) elif ( edge . geomType ( ) == 'LINE' ) and ( edge . startPoint ( ) . z == edge . endPoint ( ) . z ) : cross_section = apply_radial_line ( cross_section , edge , z_offset ) else : cross_section = apply_spline ( cross_section , edge , vertices_count [ j ] , z_offset ) return cross_section . close ( )
r"""
    Converts a thread profile to it's equivalent cross-section.

    **Profile:**

    The thread profile contains a single wire along the XZ plane
    (note: wire will be projected onto the XZ plane; Y-coords will be ignored).
    The profile is expected to be of 1 thread rotation, so it's height
    (along the Z-axis) is the thread's "pitch".
    If start_count > 1, then the profile will effectively be duplicated.
    The resulting cross-section is designed to be swept along a helical path
    with a pitch of the thread's "lead" (which is {the height of the given
    profile} * start_count)

    **Method:**

    Each edge of the profile is converted to a bezier spline, aproximating
    its polar plot equivalent.

    **Resolution:** (via `min_vertices` parameter)

    Increasing the number of vertices used to define the bezier will
    increase the resulting thread's accuracy, but cost more to render.

    min_vertices may also be expressed as a list to set the number of
    vertices to set for each wire.
    where: len(min_vertices) == number of edges in profile

    **Example**

    .. doctest::

        import cadquery
        from cqparts_fasteners.solidtypes.threads.base import profile_to_cross_section
        from Helpers import show  # doctest: +SKIP

        profile = cadquery.Workplane("XZ") \
            .moveTo(1, 0) \
            .lineTo(2, 1).lineTo(1, 2) \
            .wire()
        cross_section = profile_to_cross_section(profile)

        show(profile)  # doctest: +SKIP
        show(cross_section)  # doctest: +SKIP

    Will result in:

    .. image:: /_static/img/solidtypes.threads.base.profile_to_cross_section.01.png

    :param profile: workplane containing wire of thread profile.
    :type profile: :class:`cadquery.Workplane`
    :param lefthand: if True, cross-section is made backwards.
    :type lefthand: :class:`bool`
    :param start_count: profile is duplicated this many times.
    :type start_count: :class:`int`
    :param min_vertices: int or tuple of the desired resolution.
    :type min_vertices: :class:`int` or :class:`tuple`
    :return: workplane with a face ready to be swept into a thread.
    :rtype: :class:`cadquery.Workplane`
    :raises TypeError: if a problem is found with the given parameters.
    :raises ValueError: if ``min_vertices`` is a list with elements not equal to the numbmer of wire edges.