1#  Copyright (c) 2021, Manfred Moitzi
2#  License: MIT License
3import math
4from ezdxf.math import (
5    cubic_bezier_arc_parameters, Matrix44, Vertex, basic_transformation,
6)
7from ezdxf.render import forms
8from .path import Path
9from . import converter
10
11__all__ = [
12    "unit_circle", "elliptic_transformation", "rect", "ngon", "wedge", "star",
13    "gear"
14]
15
16
17def unit_circle(
18        start_angle: float = 0,
19        end_angle: float = math.tau,
20        segments: int = 1,
21        transform: Matrix44 = None) -> Path:
22    """ Returns an unit circle as a :class:`Path` object, with the center at
23    (0, 0, 0) and the radius of 1 drawing unit.
24
25    The arc spans from the start- to the end angle in counter clockwise
26    orientation. The end angle has to be greater than the start angle and the
27    angle span has to be greater than 0.
28
29    Args:
30        start_angle: start angle in radians
31        end_angle: end angle in radians (end_angle > start_angle!)
32        segments: count of Bèzier-curve segments, default is one segment for
33            each arc quarter (π/2)
34        transform: transformation Matrix applied to the unit circle
35
36    """
37    path = Path()
38    start_flag = True
39    for start, ctrl1, ctrl2, end in cubic_bezier_arc_parameters(
40            start_angle, end_angle, segments):
41        if start_flag:
42            path.start = start
43            start_flag = False
44        path.curve4_to(end, ctrl1, ctrl2)
45    if transform is None:
46        return path
47    else:
48        return path.transform(transform)
49
50
51def wedge(start_angle: float,
52          end_angle: float,
53          segments: int = 1,
54          transform: Matrix44 = None) -> Path:
55    """ Returns a wedge as a :class:`Path` object, with the center at
56    (0, 0, 0) and the radius of 1 drawing unit.
57
58    The arc spans from the start- to the end angle in counter clockwise
59    orientation. The end angle has to be greater than the start angle and the
60    angle span has to be greater than 0.
61
62    Args:
63        start_angle: start angle in radians
64        end_angle: end angle in radians (end_angle > start_angle!)
65        segments: count of Bèzier-curve segments, default is one segment for
66            each arc quarter (π/2)
67        transform: transformation Matrix applied to the wedge
68
69    """
70    path = Path()
71    start_flag = True
72    for start, ctrl1, ctrl2, end in cubic_bezier_arc_parameters(
73            start_angle, end_angle, segments):
74        if start_flag:
75            path.line_to(start)
76            start_flag = False
77        path.curve4_to(end, ctrl1, ctrl2)
78    path.line_to((0, 0, 0))
79    if transform is None:
80        return path
81    else:
82        return path.transform(transform)
83
84
85def elliptic_transformation(
86        center: Vertex = (0, 0, 0),
87        radius: float = 1,
88        ratio: float = 1,
89        rotation: float = 0) -> Matrix44:
90    """ Returns the transformation matrix to transform an unit circle into
91    an arbitrary circular- or elliptic arc.
92
93    Example how to create an ellipse with an major axis length of 3, a minor
94    axis length 1.5 and rotated about 90°::
95
96        m = elliptic_transformation(radius=3, ratio=0.5, rotation=math.pi / 2)
97        ellipse = shapes.unit_circle(transform=m)
98
99    Args:
100        center: curve center in WCS
101        radius: radius of the major axis in drawing units
102        ratio: ratio of minor axis to major axis
103        rotation: rotation angle about the z-axis in radians
104
105    """
106    if radius < 1e-6:
107        raise ValueError(f'invalid radius: {radius}')
108    if ratio < 1e-6:
109        raise ValueError(f'invalid ratio: {ratio}')
110    scale_x = radius
111    scale_y = radius * ratio
112    return basic_transformation(center, (scale_x, scale_y, 1), rotation)
113
114
115def rect(width: float = 1, height: float = 1,
116         transform: Matrix44 = None) -> Path:
117    """ Returns a closed rectangle as a :class:`Path` object, with the center at
118    (0, 0, 0) and the given `width` and `height` in drawing units.
119
120    Args:
121        width: width of the rectangle in drawing units, width > 0
122        height: height of the rectangle in drawing units, height > 0
123        transform: transformation Matrix applied to the rectangle
124
125    """
126    if width < 1e-9:
127        raise ValueError(f"invalid width: {width}")
128    if height < 1e-9:
129        raise ValueError(f"invalid height: {height}")
130
131    w2 = float(width) / 2.0
132    h2 = float(height) / 2.0
133    path = converter.from_vertices(
134        [(w2, h2), (-w2, h2), (-w2, -h2), (w2, h2)],
135        close=True
136    )
137    if transform is None:
138        return path
139    else:
140        return path.transform(transform)
141
142
143def ngon(count: int, length: float = None, radius: float = 1.0,
144         transform: Matrix44 = None) -> Path:
145    """ Returns a `regular polygon <https://en.wikipedia.org/wiki/Regular_polygon>`_
146    a :class:`Path` object, with the center at (0, 0, 0).
147    The polygon size is determined by the edge `length` or the circum `radius`
148    argument. If both are given `length` has higher priority. Default size is
149    a `radius` of 1. The ngon starts with the first vertex is on the x-axis!
150    The base geometry is created by function :func:`ezdxf.render.forms.ngon`.
151
152    Args:
153        count: count of polygon corners >= 3
154        length: length of polygon side
155        radius: circum radius, default is 1
156        transform: transformation Matrix applied to the ngon
157
158    """
159    vertices = forms.ngon(count, length=length, radius=radius)
160    if transform is not None:
161        vertices = transform.transform_vertices(vertices)
162    return converter.from_vertices(vertices, close=True)
163
164
165def star(count: int, r1: float, r2: float, transform: Matrix44 = None) -> Path:
166    """ Returns a `star shape <https://en.wikipedia.org/wiki/Star_polygon>`_ as
167    a :class:`Path` object, with the center at (0, 0, 0).
168
169    Argument `count` defines the count of star spikes, `r1` defines the radius
170    of the "outer" vertices and `r2` defines the radius of the "inner" vertices,
171    but this does not mean that `r1` has to be greater than `r2`.
172    The star shape starts with the first vertex is on the x-axis!
173    The base geometry is created by function :func:`ezdxf.render.forms.star`.
174
175    Args:
176        count: spike count >= 3
177        r1: radius 1
178        r2: radius 2
179        transform: transformation Matrix applied to the star
180
181    """
182    vertices = forms.star(count, r1=r1, r2=r2)
183    if transform is not None:
184        vertices = transform.transform_vertices(vertices)
185    return converter.from_vertices(vertices, close=True)
186
187
188def gear(count: int, top_width: float, bottom_width: float, height: float,
189         outside_radius: float, transform: Matrix44 = None) -> Path:
190    """
191    Returns a `gear <https://en.wikipedia.org/wiki/Gear>`_ (cogwheel) shape as
192    a :class:`Path` object, with the center at (0, 0, 0).
193    The base geometry is created by function :func:`ezdxf.render.forms.gear`.
194
195    .. warning::
196
197        This function does not create correct gears for mechanical engineering!
198
199    Args:
200        count: teeth count >= 3
201        top_width: teeth width at outside radius
202        bottom_width: teeth width at base radius
203        height: teeth height; base radius = outside radius - height
204        outside_radius: outside radius
205        transform: transformation Matrix applied to the gear shape
206
207    """
208    vertices = forms.gear(count, top_width, bottom_width, height,
209                          outside_radius)
210    if transform is not None:
211        vertices = transform.transform_vertices(vertices)
212    return converter.from_vertices(vertices, close=True)
213