1# Copyright (c) 2018-2021 Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, Iterable, List, Tuple, Sequence
4from math import pi, sin, cos, radians, tan, isclose, asin, fabs
5from enum import IntEnum
6from ezdxf.math import (
7    Vec3, Matrix44, global_bspline_interpolation, EulerSpiral,
8)
9from ezdxf.render.mesh import MeshVertexMerger, MeshTransformer
10
11if TYPE_CHECKING:
12    from ezdxf.eztypes import Vertex
13
14__all__ = [
15    "circle", "ellipse", "euler_spiral", "square", "box", "open_arrow",
16    "arrow2", "ngon", "star", "gear", "translate", "rotate", "scale",
17    "close_polygon", "cube", "extrude", "cylinder", "cylinder_2p",
18    "from_profiles_linear", "from_profiles_spline", "spline_interpolation",
19    "spline_interpolated_profiles", "cone", "cone_2p", "rotation_form",
20    "sphere",
21]
22
23
24def circle(count: int, radius: float = 1, elevation: float = 0,
25           close: bool = False) -> Iterable[Vec3]:
26    """ Create polygon vertices for a `circle <https://en.wikipedia.org/wiki/Circle>`_
27    with the given `radius` and approximated by `count` vertices, `elevation`
28    is the z-axis for all vertices.
29
30    Args:
31        count: count of polygon vertices
32        radius: circle radius
33        elevation: z-axis for all vertices
34        close: yields first vertex also as last vertex if ``True``.
35
36    Returns:
37        vertices in counter clockwise orientation as :class:`~ezdxf.math.Vec3`
38        objects
39
40    """
41    radius = float(radius)
42    delta = 2. * pi / count
43    alpha = 0.
44    for index in range(count):
45        x = cos(alpha) * radius
46        y = sin(alpha) * radius
47        yield Vec3(x, y, elevation)
48        alpha += delta
49
50    if close:
51        yield Vec3(radius, 0, elevation)
52
53
54def ellipse(count: int, rx: float = 1, ry: float = 1, start_param: float = 0,
55            end_param: float = 2 * pi, elevation: float = 0) -> Iterable[Vec3]:
56    """ Create polygon vertices for an `ellipse <https://en.wikipedia.org/wiki/Ellipse>`_
57    with given `rx` as x-axis radius and `ry` as y-axis radius approximated by
58    `count` vertices, `elevation` is the z-axis for all vertices.
59    The ellipse goes from `start_param` to `end_param` in counter clockwise
60    orientation.
61
62    Args:
63        count: count of polygon vertices
64        rx: ellipse x-axis radius
65        ry: ellipse y-axis radius
66        start_param: start of ellipse in range [0, 2π]
67        end_param: end of ellipse in range [0, 2π]
68        elevation: z-axis for all vertices
69
70    Returns:
71        vertices in counter clockwise orientation as :class:`~ezdxf.math.Vec3`
72        objects
73
74    """
75    rx = float(rx)
76    ry = float(ry)
77    start_param = float(start_param)
78    end_param = float(end_param)
79    count = int(count)
80    delta = (end_param - start_param) / (count - 1)
81    for param in range(count):
82        alpha = start_param + param * delta
83        yield Vec3(cos(alpha) * rx, sin(alpha) * ry, elevation)
84
85
86def euler_spiral(count: int, length: float = 1, curvature: float = 1,
87                 elevation: float = 0) -> Iterable[Vec3]:
88    """ Create polygon vertices for an `euler spiral <https://en.wikipedia.org/wiki/Euler_spiral>`_
89    of a given `length` and radius of curvature. This is a parametric curve,
90    which always starts at the origin (0, 0).
91
92    Args:
93        count: count of polygon vertices
94        length: length of curve in drawing units
95        curvature: radius of curvature
96        elevation: z-axis for all vertices
97
98    Returns:
99        vertices as :class:`~ezdxf.math.Vec3` objects
100
101    """
102    spiral = EulerSpiral(curvature=curvature)
103    for vertex in spiral.approximate(length, count - 1):
104        yield vertex.replace(z=elevation)
105
106
107def square(size: float = 1.) -> Tuple[Vec3, Vec3, Vec3, Vec3]:
108    """ Returns 4 vertices for a square with a side length of the given `size`,
109    lower left corner is ``(0, 0)``, upper right corner is (`size`, `size`).
110
111    """
112    return Vec3(0, 0), Vec3(size, 0), Vec3(size, size), Vec3(0, size)
113
114
115def box(sx: float = 1., sy: float = 1.) -> Tuple[Vec3, Vec3, Vec3, Vec3]:
116    """ Returns 4 vertices for a box with a width of `sx` by and a height of
117    `sy`, lower left corner is ``(0, 0)``, upper right corner is (`sx`, `sy`).
118
119    """
120    return Vec3(0, 0), Vec3(sx, 0), Vec3(sx, sy), Vec3(0, sy)
121
122
123def open_arrow(size: float = 1., angle: float = 30.) -> Tuple[Vec3, Vec3, Vec3]:
124    """ Returns 3 vertices for an open arrow `<` with a length of the given
125    `size`, argument `angle` defines the enclosing angle in degrees.
126    Vertex order: upward end vertex, tip (0, 0) , downward end vertex (counter
127    clockwise order)
128
129    Args:
130        size: length of arrow
131        angle: enclosing angle in degrees
132
133    """
134    h = sin(radians(angle / 2.)) * size
135    return Vec3(-size, h), Vec3(0, 0), Vec3(-size, -h)
136
137
138def arrow2(size: float = 1., angle: float = 30., beta: float = 45.) -> Tuple[
139    Vec3, Vec3, Vec3, Vec3]:
140    """ Returns 4 vertices for an arrow with a length of the given `size`, and
141    an enclosing `angle` in degrees and a slanted back side defined by angle
142    `beta`::
143
144                    ****
145                ****  *
146            ****     *
147        **** angle   X********************
148            ****     * +beta
149                ****  *
150                    ****
151
152                    ****
153                ****    *
154            ****         *
155        **** angle        X***************
156            ****         * -beta
157                ****    *
158                    ****
159
160    Vertex order: upward end vertex, tip (0, 0), downward end vertex, bottom
161    vertex `X` (anti clockwise order).
162
163    Bottom vertex `X` is also the connection point to a continuation line.
164
165    Args:
166        size: length of arrow
167        angle: enclosing angle in degrees
168        beta: angle if back side in degrees
169
170    """
171    h = sin(radians(angle / 2.)) * size
172    back_step = tan(radians(beta)) * h
173    return Vec3(-size, h), Vec3(0, 0), Vec3(-size, -h), \
174           Vec3(-size + back_step, 0)
175
176
177def ngon(count: int, length: float = None, radius: float = None,
178         rotation: float = 0.,
179         elevation: float = 0., close: bool = False) -> Iterable[Vec3]:
180    """ Returns the corner vertices of a `regular polygon <https://en.wikipedia.org/wiki/Regular_polygon>`_.
181    The polygon size is determined by the edge `length` or the circum `radius`
182    argument. If both are given `length` has the higher priority.
183
184    Args:
185        count: count of polygon corners >= 3
186        length: length of polygon side
187        radius: circum radius
188        rotation: rotation angle in radians
189        elevation: z-axis for all vertices
190        close: yields first vertex also as last vertex if ``True``.
191
192    Returns:
193        vertices as :class:`~ezdxf.math.Vec3` objects
194
195    """
196    if count < 3:
197        raise ValueError('Argument `count` has to be greater than 2.')
198    if length is not None:
199        if length <= 0.:
200            raise ValueError('Argument `length` has to be greater than 0.')
201        radius = length / 2. / sin(pi / count)
202    elif radius is not None:
203        if radius <= 0.:
204            raise ValueError('Argument `radius` has to be greater than 0.')
205    else:
206        raise ValueError('Argument `length` or `radius` required.')
207
208    delta = 2. * pi / count
209    angle = rotation
210    first = None
211    for _ in range(count):
212        v = Vec3(radius * cos(angle), radius * sin(angle), elevation)
213        if first is None:
214            first = v
215        yield v
216        angle += delta
217
218    if close:
219        yield first
220
221
222def star(count: int, r1: float, r2: float, rotation: float = 0.,
223         elevation: float = 0., close: bool = False) -> Iterable[Vec3]:
224    """ Returns the corner vertices for a `star shape <https://en.wikipedia.org/wiki/Star_polygon>`_.
225
226    The shape has `count` spikes, `r1` defines the radius of the "outer"
227    vertices and `r2` defines the radius of the "inner" vertices,
228    but this does not mean that `r1` has to be greater than `r2`.
229
230    Args:
231        count: spike count >= 3
232        r1: radius 1
233        r2: radius 2
234        rotation: rotation angle in radians
235        elevation: z-axis for all vertices
236        close: yields first vertex also as last vertex if ``True``.
237
238    Returns:
239        vertices as :class:`~ezdxf.math.Vec3` objects
240
241    """
242    if count < 3:
243        raise ValueError('Argument `count` has to be greater than 2.')
244    if r1 <= 0.:
245        raise ValueError('Argument `r1` has to be greater than 0.')
246    if r2 <= 0.:
247        raise ValueError('Argument `r2` has to be greater than 0.')
248
249    corners1 = ngon(count, radius=r1, rotation=rotation, elevation=elevation,
250                    close=False)
251    corners2 = ngon(count, radius=r2, rotation=pi / count + rotation,
252                    elevation=elevation, close=False)
253    first = None
254    for s1, s2 in zip(corners1, corners2):
255        if first is None:
256            first = s1
257        yield s1
258        yield s2
259
260    if close:
261        yield first
262
263
264class _Gear(IntEnum):
265    TOP_START = 0
266    TOP_END = 1
267    BOTTOM_START = 2
268    BOTTOM_END = 3
269
270
271def gear(count: int, top_width: float, bottom_width: float, height: float,
272         outside_radius: float, elevation: float = 0,
273         close: bool = False) -> Iterable[Vec3]:
274    """ Returns the corner vertices of a `gear shape <https://en.wikipedia.org/wiki/Gear>`_
275    (cogwheel).
276
277    .. warning::
278
279        This function does not create correct gears for mechanical engineering!
280
281    Args:
282        count: teeth count >= 3
283        top_width: teeth width at outside radius
284        bottom_width: teeth width at base radius
285        height: teeth height; base radius = outside radius - height
286        outside_radius: outside radius
287        elevation: z-axis for all vertices
288        close: yields first vertex also as last vertex if True.
289
290    Returns:
291        vertices in counter clockwise orientation as :class:`~ezdxf.math.Vec3`
292        objects
293
294    """
295    if count < 3:
296        raise ValueError('Argument `count` has to be greater than 2.')
297    if outside_radius <= 0.:
298        raise ValueError('Argument `radius` has to be greater than 0.')
299    if top_width <= 0.:
300        raise ValueError('Argument `width` has to be greater than 0.')
301    if bottom_width <= 0.:
302        raise ValueError('Argument `width` has to be greater than 0.')
303    if height <= 0.:
304        raise ValueError('Argument `height` has to be greater than 0.')
305    if height >= outside_radius:
306        raise ValueError('Argument `height` has to be smaller than `radius`')
307
308    base_radius = outside_radius - height
309    alpha_top = asin(top_width / 2. / outside_radius)  # angle at tooth top
310    alpha_bottom = asin(
311        bottom_width / 2. / base_radius)  # angle at tooth bottom
312    alpha_difference = (
313                               alpha_bottom - alpha_top) / 2.  # alpha difference at start and end of tooth
314    beta = (2. * pi - count * alpha_bottom) / count
315    angle = -alpha_top / 2.  # center of first tooth is in x-axis direction
316    state = _Gear.TOP_START
317    first = None
318    for _ in range(4 * count):
319        if state == _Gear.TOP_START or state == _Gear.TOP_END:
320            radius = outside_radius
321        else:
322            radius = base_radius
323        v = Vec3(radius * cos(angle), radius * sin(angle), elevation)
324
325        if state == _Gear.TOP_START:
326            angle += alpha_top
327        elif state == _Gear.TOP_END:
328            angle += alpha_difference
329        elif state == _Gear.BOTTOM_START:
330            angle += beta
331        elif state == _Gear.BOTTOM_END:
332            angle += alpha_difference
333
334        if first is None:
335            first = v
336        yield v
337
338        state += 1
339        if state > _Gear.BOTTOM_END:
340            state = _Gear.TOP_START
341
342    if close:
343        yield first
344
345
346def translate(vertices: Iterable['Vertex'], vec: 'Vertex' = (0, 0, 0)) -> \
347        Iterable[Vec3]:
348    """ Translate `vertices` along `vec`, faster than a Matrix44 transformation.
349
350    Args:
351        vertices: iterable of vertices
352        vec: translation vector
353
354    Returns: yields transformed vertices
355
356    """
357    vec = Vec3(vec)
358    for p in vertices:
359        yield vec + p
360
361
362def rotate(vertices: Iterable['Vertex'], angle: 0., deg: bool = True) -> \
363        Iterable[Vec3]:
364    """ Rotate `vertices` about to z-axis at to origin (0, 0), faster than a
365    Matrix44 transformation.
366
367    Args:
368        vertices: iterable of vertices
369        angle: rotation angle
370        deg: True if angle in degrees, False if angle in radians
371
372    Returns: yields transformed vertices
373
374    """
375    if deg:
376        return (Vec3(v).rotate_deg(angle) for v in vertices)
377    else:
378        return (Vec3(v).rotate(angle) for v in vertices)
379
380
381def scale(vertices: Iterable['Vertex'], scaling=(1., 1., 1.)) -> Iterable[Vec3]:
382    """ Scale `vertices` around the origin (0, 0), faster than a Matrix44
383    transformation.
384
385    Args:
386        vertices: iterable of vertices
387        scaling: scale factors as tuple of floats for x-, y- and z-axis
388
389    Returns: yields scaled vertices
390
391    """
392    sx, sy, sz = scaling
393    for v in vertices:
394        v = Vec3(v)
395        yield Vec3(v.x * sx, v.y * sy, v.z * sz)
396
397
398def close_polygon(vertices: Iterable['Vertex'],
399                  rel_tol: float = 1e-9,
400                  abs_tol: float = 1e-12) -> List['Vertex']:
401    """ Returns list of vertices, where vertices[0] == vertices[-1].
402    """
403    vertices = list(vertices)
404    if not Vec3(vertices[0]).isclose(
405            vertices[-1], rel_tol=rel_tol, abs_tol=abs_tol):
406        vertices.append(vertices[0])
407    return vertices
408
409
410# 8 corner vertices
411_cube_vertices = [
412    Vec3(0, 0, 0),
413    Vec3(1, 0, 0),
414    Vec3(1, 1, 0),
415    Vec3(0, 1, 0),
416    Vec3(0, 0, 1),
417    Vec3(1, 0, 1),
418    Vec3(1, 1, 1),
419    Vec3(0, 1, 1),
420]
421
422# 8 corner vertices, 'mass' center in (0, 0, 0)
423_cube0_vertices = [
424    Vec3(-.5, -.5, -.5),
425    Vec3(+.5, -.5, -.5),
426    Vec3(+.5, +.5, -.5),
427    Vec3(-.5, +.5, -.5),
428    Vec3(-.5, -.5, +.5),
429    Vec3(+.5, -.5, +.5),
430    Vec3(+.5, +.5, +.5),
431    Vec3(-.5, +.5, +.5),
432]
433
434# 6 cube faces
435cube_faces = [
436    [0, 3, 2, 1],
437    [4, 5, 6, 7],
438    [0, 1, 5, 4],
439    [1, 2, 6, 5],
440    [3, 7, 6, 2],
441    [0, 4, 7, 3],
442]
443
444
445def cube(center: bool = True) -> MeshTransformer:
446    """ Create a `cube <https://en.wikipedia.org/wiki/Cube>`_ as
447    :class:`~ezdxf.render.MeshTransformer` object.
448
449    Args:
450        center: 'mass' center of cube, ``(0, 0, 0)`` if ``True``, else first
451            corner at ``(0, 0, 0)``
452
453    Returns: :class:`~ezdxf.render.MeshTransformer`
454
455    """
456    mesh = MeshTransformer()
457    vectices = _cube0_vertices if center else _cube_vertices
458    mesh.add_mesh(vertices=vectices, faces=cube_faces)
459    return mesh
460
461
462def extrude(profile: Iterable['Vertex'], path: Iterable['Vertex'],
463            close=True) -> MeshTransformer:
464    """ Extrude a `profile` polygon along a `path` polyline, vertices of profile
465    should be in counter clockwise order.
466
467    Args:
468        profile: sweeping profile as list of (x, y, z) tuples in counter
469            clockwise order
470        path:  extrusion path as list of (x, y, z) tuples
471        close: close profile polygon if ``True``
472
473    Returns: :class:`~ezdxf.render.MeshTransformer`
474
475    """
476
477    def add_hull(bottom_profile, top_profile):
478        prev_bottom = bottom_profile[0]
479        prev_top = top_profile[0]
480        for bottom, top in zip(bottom_profile[1:], top_profile[1:]):
481            face = (prev_bottom, bottom, top,
482                    prev_top)  # counter clock wise: normals outwards
483            mesh.faces.append(face)
484            prev_bottom = bottom
485            prev_top = top
486
487    mesh = MeshVertexMerger()
488    if close:
489        profile = close_polygon(profile)
490    profile = [Vec3(p) for p in profile]
491    path = [Vec3(p) for p in path]
492    start_point = path[0]
493    bottom_indices = mesh.add_vertices(profile)  # base profile
494    for target_point in path[1:]:
495        translation_vector = target_point - start_point
496        # profile will just be translated
497        profile = [vec + translation_vector for vec in profile]
498        top_indices = mesh.add_vertices(profile)
499        add_hull(bottom_indices, top_indices)
500        bottom_indices = top_indices
501        start_point = target_point
502    return MeshTransformer.from_builder(mesh)
503
504
505def cylinder(count: int = 16, radius: float = 1., top_radius: float = None,
506             top_center: 'Vertex' = (0, 0, 1),
507             caps=True, ngons=True) -> MeshTransformer:
508    """ Create a `cylinder <https://en.wikipedia.org/wiki/Cylinder>`_ as
509    :class:`~ezdxf.render.MeshTransformer` object, the base center is fixed in
510    the origin (0, 0, 0).
511
512    Args:
513        count: profiles edge count
514        radius: radius for bottom profile
515        top_radius: radius for top profile, if ``None`` top_radius == radius
516        top_center: location vector for the center of the top profile
517        caps: close hull with bottom cap and top cap (as N-gons)
518        ngons: use ngons for caps if ``True`` else subdivide caps into triangles
519
520    Returns: :class:`~ezdxf.render.MeshTransformer`
521
522    """
523    if top_radius is None:
524        top_radius = radius
525
526    if isclose(top_radius, 0.):  # pyramid/cone
527        return cone(count=count, radius=radius, apex=top_center)
528
529    base_profile = list(circle(count, radius, close=True))
530    top_profile = list(
531        translate(circle(count, top_radius, close=True), top_center))
532    return from_profiles_linear([base_profile, top_profile], caps=caps,
533                                ngons=ngons)
534
535
536def cylinder_2p(count: int = 16, radius: float = 1, base_center=(0, 0, 0),
537                top_center=(0, 0, 1), ) -> MeshTransformer:
538    """ Create a `cylinder <https://en.wikipedia.org/wiki/Cylinder>`_ as
539    :class:`~ezdxf.render.MeshTransformer` object from two points,
540    `base_center` is the center of the base circle and, `top_center` the center
541    of the top circle.
542
543    Args:
544        count: profiles edge count
545        radius: radius for bottom profile
546        base_center: center of base circle
547        top_center: center of top circle
548
549    Returns: :class:`~ezdxf.render.MeshTransformer`
550
551    """
552    # Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license.
553    # Python port Copyright (c) 2012 Tim Knip (http://www.floorplanner.com), under the MIT license.
554    # Additions by Alex Pletzer (Pennsylvania State University)
555    # Adaptation for ezdxf, Copyright (c) 2020, Manfred Moitzi, MIT License.
556    start = Vec3(base_center)
557    end = Vec3(top_center)
558    radius = float(radius)
559    slices = int(count)
560    ray = end - start
561
562    z_axis = ray.normalize()
563    is_y = (fabs(z_axis.y) > 0.5)
564    x_axis = Vec3(float(is_y), float(not is_y), 0).cross(z_axis).normalize()
565    y_axis = x_axis.cross(z_axis).normalize()
566    mesh = MeshVertexMerger()
567
568    def vertex(stack, angle):
569        out = (x_axis * cos(angle)) + (y_axis * sin(angle))
570        return start + (ray * stack) + (out * radius)
571
572    dt = pi * 2 / float(slices)
573    for i in range(0, slices):
574        t0 = i * dt
575        i1 = (i + 1) % slices
576        t1 = i1 * dt
577        mesh.add_face([start, vertex(0, t0), vertex(0, t1)])
578        mesh.add_face(
579            [vertex(0, t1), vertex(0, t0), vertex(1, t0), vertex(1, t1)])
580        mesh.add_face([end, vertex(1, t1), vertex(1, t0)])
581    return MeshTransformer.from_builder(mesh)
582
583
584def ngon_to_triangles(face: Iterable['Vertex']) -> Iterable[Sequence[Vec3]]:
585    face = [Vec3(v) for v in face]
586    if face[0].isclose(face[-1]):  # closed shape
587        center = Vec3.sum(face[:-1]) / (len(face) - 1)
588    else:
589        center = Vec3.sum(face) / len(face)
590        face.append(face[0])
591
592    for v1, v2 in zip(face[:-1], face[1:]):
593        yield v1, v2, center
594
595
596def from_profiles_linear(profiles: Iterable[Iterable['Vertex']], close=True,
597                         caps=False, ngons=True) -> MeshTransformer:
598    """ Create MESH entity by linear connected `profiles`.
599
600    Args:
601        profiles: list of profiles
602        close: close profile polygon if ``True``
603        caps: close hull with bottom cap and top cap
604        ngons: use ngons for caps if ``True`` else subdivide caps into triangles
605
606    Returns: :class:`~ezdxf.render.MeshTransformer`
607
608    """
609    mesh = MeshVertexMerger()
610    profiles = list(profiles)
611    if close:
612        profiles = [close_polygon(p) for p in profiles]
613    if caps:
614        base = reversed(profiles[0])  # for correct outside pointing normals
615        top = profiles[-1]
616        if ngons:
617            mesh.add_face(base)
618            mesh.add_face(top)
619        else:
620            for face in ngon_to_triangles(base):
621                mesh.add_face(face)
622            for face in ngon_to_triangles(top):
623                mesh.add_face(face)
624
625    for profile1, profile2 in zip(profiles, profiles[1:]):
626        prev_v1, prev_v2 = None, None
627        for v1, v2 in zip(profile1, profile2):
628            if prev_v1 is not None:
629                mesh.add_face([prev_v1, v1, v2, prev_v2])
630            prev_v1 = v1
631            prev_v2 = v2
632
633    return MeshTransformer.from_builder(mesh)
634
635
636def spline_interpolation(vertices: Iterable['Vertex'], degree: int = 3,
637                         method: str = 'chord',
638                         subdivide: int = 4) -> List[Vec3]:
639    """ B-spline interpolation, vertices are fit points for the spline
640    definition.
641
642    Only method 'uniform', yields vertices at fit points.
643
644    Args:
645        vertices: fit points
646        degree: degree of B-spline
647        method: "uniform", "chord"/"distance", "centripetal"/"sqrt_chord" or
648            "arc" calculation method for parameter t
649        subdivide: count of sub vertices + 1, e.g. 4 creates 3 sub-vertices
650
651    Returns: list of vertices
652
653    """
654    vertices = list(vertices)
655    spline = global_bspline_interpolation(vertices, degree=degree,
656                                          method=method)
657    return list(spline.approximate(segments=(len(vertices) - 1) * subdivide))
658
659
660def spline_interpolated_profiles(profiles: Iterable[Iterable['Vertex']],
661                                 subdivide: int = 4) -> Iterable[List[Vec3]]:
662    """ Profile interpolation by cubic B-spline interpolation.
663
664    Args:
665        profiles: list of profiles
666        subdivide: count of interpolated profiles + 1, e.g. 4 creates 3
667            sub-profiles between two main profiles (4 face loops)
668
669    Returns: yields profiles as list of vertices
670
671    """
672    profiles = [list(p) for p in profiles]
673    if len(set(len(p) for p in profiles)) != 1:
674        raise ValueError('All profiles have to have the same vertex count')
675
676    vertex_count = len(profiles[0])
677    edges = []  # interpolated spline vertices, where profile vertices are fit points
678    for index in range(vertex_count):
679        edge_vertices = [p[index] for p in profiles]
680        edges.append(spline_interpolation(edge_vertices, subdivide=subdivide))
681
682    profile_count = len(edges[0])
683    for profile_index in range(profile_count):
684        yield [edge[profile_index] for edge in edges]
685
686
687def from_profiles_spline(profiles: Iterable[Iterable['Vertex']],
688                         subdivide: int = 4, close=True,
689                         caps=False, ngons=True) -> MeshTransformer:
690    """ Create MESH entity by spline interpolation between given `profiles`.
691    Requires at least 4 profiles. A subdivide value of 4, means, create 4 face
692    loops between two profiles, without interpolation two profiles create one
693    face loop.
694
695    Args:
696        profiles: list of profiles
697        subdivide: count of face loops
698        close: close profile polygon if ``True``
699        caps: close hull with bottom cap and top cap
700        ngons: use ngons for caps if ``True`` else subdivide caps into triangles
701
702    Returns: :class:`~ezdxf.render.MeshTransformer`
703
704    """
705    profiles = list(profiles)
706    if len(profiles) > 3:
707        profiles = spline_interpolated_profiles(profiles, subdivide)
708    else:
709        raise ValueError("Spline interpolation requires at least 4 profiles")
710    return from_profiles_linear(profiles, close=close, caps=caps, ngons=ngons)
711
712
713def cone(count: int = 16, radius: float = 1.0, apex: 'Vertex' = (0, 0, 1),
714         caps=True, ngons=True) -> MeshTransformer:
715    """ Create a `cone <https://en.wikipedia.org/wiki/Cone>`_ as
716    :class:`~ezdxf.render.MeshTransformer` object, the base center is fixed in
717    the origin (0, 0, 0).
718
719    Args:
720        count: edge count of basis_vector
721        radius: radius of basis_vector
722        apex: tip of the cone
723        caps: add a bottom face if ``True``
724        ngons: use ngons for caps if ``True`` else subdivide caps into triangles
725
726    Returns: :class:`~ezdxf.render.MeshTransformer`
727
728    """
729    mesh = MeshVertexMerger()
730    base_circle = list(circle(count, radius, close=True))
731    for p1, p2 in zip(base_circle, base_circle[1:]):
732        mesh.add_face([p1, p2, apex])
733    if caps:
734        base_circle = reversed(
735            base_circle)  # for correct outside pointing normals
736        if ngons:
737            mesh.add_face(base_circle)
738        else:
739            for face in ngon_to_triangles(base_circle):
740                mesh.add_face(face)
741
742    return MeshTransformer.from_builder(mesh)
743
744
745def cone_2p(count: int = 16, radius: float = 1.0, base_center=(0, 0, 0),
746            apex=(0, 0, 1)) -> MeshTransformer:
747    """ Create a `cone <https://en.wikipedia.org/wiki/Cone>`_ as
748    :class:`~ezdxf.render.MeshTransformer` object from two points, `base_center`
749    is the center of the base circle and `apex` as the tip of the cone.
750
751    Args:
752        count: edge count of basis_vector
753        radius: radius of basis_vector
754        base_center: center point of base circle
755        apex: tip of the cone
756
757    Returns: :class:`~ezdxf.render.MeshTransformer`
758
759    """
760    # Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license.
761    # Python port Copyright (c) 2012 Tim Knip (http://www.floorplanner.com), under the MIT license.
762    # Additions by Alex Pletzer (Pennsylvania State University)
763    # Adaptation for ezdxf, Copyright (c) 2020, Manfred Moitzi, MIT License.
764    start = Vec3(base_center)
765    end = Vec3(apex)
766    slices = int(count)
767    ray = end - start
768    z_axis = ray.normalize()
769    is_y = (fabs(z_axis.y) > 0.5)
770    x_axis = Vec3(float(is_y), float(not is_y), 0).cross(z_axis).normalize()
771    y_axis = x_axis.cross(z_axis).normalize()
772    mesh = MeshVertexMerger()
773
774    def vertex(angle) -> Vec3:
775        # radial direction pointing out
776        out = x_axis * cos(angle) + y_axis * sin(angle)
777        return start + out * radius
778
779    dt = pi * 2.0 / slices
780    for i in range(0, slices):
781        t0 = i * dt
782        i1 = (i + 1) % slices
783        t1 = i1 * dt
784        # coordinates and associated normal pointing outwards of the cone's
785        # side
786        p0 = vertex(t0)
787        p1 = vertex(t1)
788        # polygon on the low side (disk sector)
789        mesh.add_face([start, p0, p1])
790        # polygon extending from the low side to the tip
791        mesh.add_face([p0, end, p1])
792
793    return MeshTransformer.from_builder(mesh)
794
795
796def rotation_form(count: int, profile: Iterable['Vertex'],
797                  angle: float = 2 * pi,
798                  axis: 'Vertex' = (1, 0, 0)) -> MeshTransformer:
799    """ Create MESH entity by rotating a `profile` around an `axis`.
800
801    Args:
802        count: count of rotated profiles
803        profile: profile to rotate as list of vertices
804        angle: rotation angle in radians
805        axis: rotation axis
806
807    Returns: :class:`~ezdxf.render.MeshTransformer`
808
809    """
810    if count < 3:
811        raise ValueError('count >= 2')
812    delta = float(angle) / count
813    m = Matrix44.axis_rotate(Vec3(axis), delta)
814    profile = [Vec3(p) for p in profile]
815    profiles = [profile]
816    for _ in range(int(count)):
817        profile = list(m.transform_vertices(profile))
818        profiles.append(profile)
819    mesh = from_profiles_linear(profiles, close=False, caps=False)
820    return mesh
821
822
823def sphere(count: int = 16, stacks: int = 8, radius: float = 1,
824           quads=True) -> MeshTransformer:
825    """ Create a `sphere <https://en.wikipedia.org/wiki/Sphere>`_ as
826    :class:`~ezdxf.render.MeshTransformer` object, center is fixed at origin
827    (0, 0, 0).
828
829    Args:
830        count: longitudinal slices
831        stacks: latitude slices
832        radius: radius of sphere
833        quads: use quads for body faces if ``True`` else triangles
834
835    Returns: :class:`~ezdxf.render.MeshTransformer`
836
837    """
838    radius = float(radius)
839    slices = int(count)
840    stacks_2 = int(stacks) // 2  # stacks from -stack/2 to +stack/2
841    delta_theta = pi * 2.0 / float(slices)
842    delta_phi = pi / float(stacks)
843    mesh = MeshVertexMerger()
844
845    def radius_of_stack(stack: float) -> float:
846        return radius * cos(delta_phi * stack)
847
848    def vertex(slice_: float, r: float, z: float) -> Vec3:
849        actual_theta = delta_theta * slice_
850        return Vec3(cos(actual_theta) * r, sin(actual_theta) * r, z)
851
852    def cap_triangles(stack, top=False):
853        z = sin(stack * delta_phi) * radius
854        cap_vertex = Vec3(0, 0, radius) if top else Vec3(0, 0, -radius)
855        r1 = radius_of_stack(stack)
856        for slice_ in range(slices):
857            v1 = vertex(slice_, r1, z)
858            v2 = vertex(slice_ + 1, r1, z)
859            if top:
860                mesh.add_face((v1, v2, cap_vertex))
861            else:
862                mesh.add_face((cap_vertex, v2, v1))
863
864    # bottom triangle faces
865    cap_triangles(-stacks_2 + 1, top=False)
866
867    # add body faces
868    for actual_stack in range(-stacks_2 + 1, stacks_2 - 1):
869        next_stack = actual_stack + 1
870        r1 = radius_of_stack(actual_stack)
871        r2 = radius_of_stack(next_stack)
872        z1 = sin(delta_phi * actual_stack) * radius
873        z2 = sin(delta_phi * next_stack) * radius
874        for i in range(slices):
875            v1 = vertex(i, r1, z1)
876            v2 = vertex(i + 1, r1, z1)
877            v3 = vertex(i + 1, r2, z2)
878            v4 = vertex(i, r2, z2)
879            if quads:
880                mesh.add_face([v1, v2, v3, v4])
881            else:
882                center = vertex(
883                    i + 0.5,
884                    radius_of_stack(actual_stack + 0.5),
885                    sin(delta_phi * (actual_stack + 0.5)) * radius,
886                )
887                mesh.add_face([v1, v2, center])
888                mesh.add_face([v2, v3, center])
889                mesh.add_face([v3, v4, center])
890                mesh.add_face([v4, v1, center])
891
892    # top triangle faces
893    cap_triangles(stacks_2 - 1, top=True)
894
895    return MeshTransformer.from_builder(mesh)
896