xref: /aosp_15_r20/external/fonttools/Lib/fontTools/svgLib/path/arc.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1*e1fe3e4aSElliott Hughes"""Convert SVG Path's elliptical arcs to Bezier curves.
2*e1fe3e4aSElliott Hughes
3*e1fe3e4aSElliott HughesThe code is mostly adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic
4*e1fe3e4aSElliott Hugheshttps://github.com/chromium/chromium/blob/93831f2/third_party/
5*e1fe3e4aSElliott Hughesblink/renderer/core/svg/svg_path_parser.cc#L169-L278
6*e1fe3e4aSElliott Hughes"""
7*e1fe3e4aSElliott Hughes
8*e1fe3e4aSElliott Hughesfrom fontTools.misc.transform import Identity, Scale
9*e1fe3e4aSElliott Hughesfrom math import atan2, ceil, cos, fabs, isfinite, pi, radians, sin, sqrt, tan
10*e1fe3e4aSElliott Hughes
11*e1fe3e4aSElliott Hughes
12*e1fe3e4aSElliott HughesTWO_PI = 2 * pi
13*e1fe3e4aSElliott HughesPI_OVER_TWO = 0.5 * pi
14*e1fe3e4aSElliott Hughes
15*e1fe3e4aSElliott Hughes
16*e1fe3e4aSElliott Hughesdef _map_point(matrix, pt):
17*e1fe3e4aSElliott Hughes    # apply Transform matrix to a point represented as a complex number
18*e1fe3e4aSElliott Hughes    r = matrix.transformPoint((pt.real, pt.imag))
19*e1fe3e4aSElliott Hughes    return r[0] + r[1] * 1j
20*e1fe3e4aSElliott Hughes
21*e1fe3e4aSElliott Hughes
22*e1fe3e4aSElliott Hughesclass EllipticalArc(object):
23*e1fe3e4aSElliott Hughes    def __init__(self, current_point, rx, ry, rotation, large, sweep, target_point):
24*e1fe3e4aSElliott Hughes        self.current_point = current_point
25*e1fe3e4aSElliott Hughes        self.rx = rx
26*e1fe3e4aSElliott Hughes        self.ry = ry
27*e1fe3e4aSElliott Hughes        self.rotation = rotation
28*e1fe3e4aSElliott Hughes        self.large = large
29*e1fe3e4aSElliott Hughes        self.sweep = sweep
30*e1fe3e4aSElliott Hughes        self.target_point = target_point
31*e1fe3e4aSElliott Hughes
32*e1fe3e4aSElliott Hughes        # SVG arc's rotation angle is expressed in degrees, whereas Transform.rotate
33*e1fe3e4aSElliott Hughes        # uses radians
34*e1fe3e4aSElliott Hughes        self.angle = radians(rotation)
35*e1fe3e4aSElliott Hughes
36*e1fe3e4aSElliott Hughes        # these derived attributes are computed by the _parametrize method
37*e1fe3e4aSElliott Hughes        self.center_point = self.theta1 = self.theta2 = self.theta_arc = None
38*e1fe3e4aSElliott Hughes
39*e1fe3e4aSElliott Hughes    def _parametrize(self):
40*e1fe3e4aSElliott Hughes        # convert from endopoint to center parametrization:
41*e1fe3e4aSElliott Hughes        # https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
42*e1fe3e4aSElliott Hughes
43*e1fe3e4aSElliott Hughes        # If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a
44*e1fe3e4aSElliott Hughes        # "lineto") joining the endpoints.
45*e1fe3e4aSElliott Hughes        # http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
46*e1fe3e4aSElliott Hughes        rx = fabs(self.rx)
47*e1fe3e4aSElliott Hughes        ry = fabs(self.ry)
48*e1fe3e4aSElliott Hughes        if not (rx and ry):
49*e1fe3e4aSElliott Hughes            return False
50*e1fe3e4aSElliott Hughes
51*e1fe3e4aSElliott Hughes        # If the current point and target point for the arc are identical, it should
52*e1fe3e4aSElliott Hughes        # be treated as a zero length path. This ensures continuity in animations.
53*e1fe3e4aSElliott Hughes        if self.target_point == self.current_point:
54*e1fe3e4aSElliott Hughes            return False
55*e1fe3e4aSElliott Hughes
56*e1fe3e4aSElliott Hughes        mid_point_distance = (self.current_point - self.target_point) * 0.5
57*e1fe3e4aSElliott Hughes
58*e1fe3e4aSElliott Hughes        point_transform = Identity.rotate(-self.angle)
59*e1fe3e4aSElliott Hughes
60*e1fe3e4aSElliott Hughes        transformed_mid_point = _map_point(point_transform, mid_point_distance)
61*e1fe3e4aSElliott Hughes        square_rx = rx * rx
62*e1fe3e4aSElliott Hughes        square_ry = ry * ry
63*e1fe3e4aSElliott Hughes        square_x = transformed_mid_point.real * transformed_mid_point.real
64*e1fe3e4aSElliott Hughes        square_y = transformed_mid_point.imag * transformed_mid_point.imag
65*e1fe3e4aSElliott Hughes
66*e1fe3e4aSElliott Hughes        # Check if the radii are big enough to draw the arc, scale radii if not.
67*e1fe3e4aSElliott Hughes        # http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
68*e1fe3e4aSElliott Hughes        radii_scale = square_x / square_rx + square_y / square_ry
69*e1fe3e4aSElliott Hughes        if radii_scale > 1:
70*e1fe3e4aSElliott Hughes            rx *= sqrt(radii_scale)
71*e1fe3e4aSElliott Hughes            ry *= sqrt(radii_scale)
72*e1fe3e4aSElliott Hughes            self.rx, self.ry = rx, ry
73*e1fe3e4aSElliott Hughes
74*e1fe3e4aSElliott Hughes        point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle)
75*e1fe3e4aSElliott Hughes
76*e1fe3e4aSElliott Hughes        point1 = _map_point(point_transform, self.current_point)
77*e1fe3e4aSElliott Hughes        point2 = _map_point(point_transform, self.target_point)
78*e1fe3e4aSElliott Hughes        delta = point2 - point1
79*e1fe3e4aSElliott Hughes
80*e1fe3e4aSElliott Hughes        d = delta.real * delta.real + delta.imag * delta.imag
81*e1fe3e4aSElliott Hughes        scale_factor_squared = max(1 / d - 0.25, 0.0)
82*e1fe3e4aSElliott Hughes
83*e1fe3e4aSElliott Hughes        scale_factor = sqrt(scale_factor_squared)
84*e1fe3e4aSElliott Hughes        if self.sweep == self.large:
85*e1fe3e4aSElliott Hughes            scale_factor = -scale_factor
86*e1fe3e4aSElliott Hughes
87*e1fe3e4aSElliott Hughes        delta *= scale_factor
88*e1fe3e4aSElliott Hughes        center_point = (point1 + point2) * 0.5
89*e1fe3e4aSElliott Hughes        center_point += complex(-delta.imag, delta.real)
90*e1fe3e4aSElliott Hughes        point1 -= center_point
91*e1fe3e4aSElliott Hughes        point2 -= center_point
92*e1fe3e4aSElliott Hughes
93*e1fe3e4aSElliott Hughes        theta1 = atan2(point1.imag, point1.real)
94*e1fe3e4aSElliott Hughes        theta2 = atan2(point2.imag, point2.real)
95*e1fe3e4aSElliott Hughes
96*e1fe3e4aSElliott Hughes        theta_arc = theta2 - theta1
97*e1fe3e4aSElliott Hughes        if theta_arc < 0 and self.sweep:
98*e1fe3e4aSElliott Hughes            theta_arc += TWO_PI
99*e1fe3e4aSElliott Hughes        elif theta_arc > 0 and not self.sweep:
100*e1fe3e4aSElliott Hughes            theta_arc -= TWO_PI
101*e1fe3e4aSElliott Hughes
102*e1fe3e4aSElliott Hughes        self.theta1 = theta1
103*e1fe3e4aSElliott Hughes        self.theta2 = theta1 + theta_arc
104*e1fe3e4aSElliott Hughes        self.theta_arc = theta_arc
105*e1fe3e4aSElliott Hughes        self.center_point = center_point
106*e1fe3e4aSElliott Hughes
107*e1fe3e4aSElliott Hughes        return True
108*e1fe3e4aSElliott Hughes
109*e1fe3e4aSElliott Hughes    def _decompose_to_cubic_curves(self):
110*e1fe3e4aSElliott Hughes        if self.center_point is None and not self._parametrize():
111*e1fe3e4aSElliott Hughes            return
112*e1fe3e4aSElliott Hughes
113*e1fe3e4aSElliott Hughes        point_transform = Identity.rotate(self.angle).scale(self.rx, self.ry)
114*e1fe3e4aSElliott Hughes
115*e1fe3e4aSElliott Hughes        # Some results of atan2 on some platform implementations are not exact
116*e1fe3e4aSElliott Hughes        # enough. So that we get more cubic curves than expected here. Adding 0.001f
117*e1fe3e4aSElliott Hughes        # reduces the count of sgements to the correct count.
118*e1fe3e4aSElliott Hughes        num_segments = int(ceil(fabs(self.theta_arc / (PI_OVER_TWO + 0.001))))
119*e1fe3e4aSElliott Hughes        for i in range(num_segments):
120*e1fe3e4aSElliott Hughes            start_theta = self.theta1 + i * self.theta_arc / num_segments
121*e1fe3e4aSElliott Hughes            end_theta = self.theta1 + (i + 1) * self.theta_arc / num_segments
122*e1fe3e4aSElliott Hughes
123*e1fe3e4aSElliott Hughes            t = (4 / 3) * tan(0.25 * (end_theta - start_theta))
124*e1fe3e4aSElliott Hughes            if not isfinite(t):
125*e1fe3e4aSElliott Hughes                return
126*e1fe3e4aSElliott Hughes
127*e1fe3e4aSElliott Hughes            sin_start_theta = sin(start_theta)
128*e1fe3e4aSElliott Hughes            cos_start_theta = cos(start_theta)
129*e1fe3e4aSElliott Hughes            sin_end_theta = sin(end_theta)
130*e1fe3e4aSElliott Hughes            cos_end_theta = cos(end_theta)
131*e1fe3e4aSElliott Hughes
132*e1fe3e4aSElliott Hughes            point1 = complex(
133*e1fe3e4aSElliott Hughes                cos_start_theta - t * sin_start_theta,
134*e1fe3e4aSElliott Hughes                sin_start_theta + t * cos_start_theta,
135*e1fe3e4aSElliott Hughes            )
136*e1fe3e4aSElliott Hughes            point1 += self.center_point
137*e1fe3e4aSElliott Hughes            target_point = complex(cos_end_theta, sin_end_theta)
138*e1fe3e4aSElliott Hughes            target_point += self.center_point
139*e1fe3e4aSElliott Hughes            point2 = target_point
140*e1fe3e4aSElliott Hughes            point2 += complex(t * sin_end_theta, -t * cos_end_theta)
141*e1fe3e4aSElliott Hughes
142*e1fe3e4aSElliott Hughes            point1 = _map_point(point_transform, point1)
143*e1fe3e4aSElliott Hughes            point2 = _map_point(point_transform, point2)
144*e1fe3e4aSElliott Hughes            target_point = _map_point(point_transform, target_point)
145*e1fe3e4aSElliott Hughes
146*e1fe3e4aSElliott Hughes            yield point1, point2, target_point
147*e1fe3e4aSElliott Hughes
148*e1fe3e4aSElliott Hughes    def draw(self, pen):
149*e1fe3e4aSElliott Hughes        for point1, point2, target_point in self._decompose_to_cubic_curves():
150*e1fe3e4aSElliott Hughes            pen.curveTo(
151*e1fe3e4aSElliott Hughes                (point1.real, point1.imag),
152*e1fe3e4aSElliott Hughes                (point2.real, point2.imag),
153*e1fe3e4aSElliott Hughes                (target_point.real, target_point.imag),
154*e1fe3e4aSElliott Hughes            )
155