|
"""Convert SVG Path's elliptical arcs to Bezier curves. |
|
|
|
The code is mostly adapted from Blink's SVGPathNormalizer::DecomposeArcToCubic |
|
https://github.com/chromium/chromium/blob/93831f2/third_party/ |
|
blink/renderer/core/svg/svg_path_parser.cc#L169-L278 |
|
""" |
|
from fontTools.misc.transform import Identity, Scale |
|
from math import atan2, ceil, cos, fabs, isfinite, pi, radians, sin, sqrt, tan |
|
|
|
|
|
TWO_PI = 2 * pi |
|
PI_OVER_TWO = 0.5 * pi |
|
|
|
|
|
def _map_point(matrix, pt): |
|
|
|
r = matrix.transformPoint((pt.real, pt.imag)) |
|
return r[0] + r[1] * 1j |
|
|
|
|
|
class EllipticalArc(object): |
|
def __init__(self, current_point, rx, ry, rotation, large, sweep, target_point): |
|
self.current_point = current_point |
|
self.rx = rx |
|
self.ry = ry |
|
self.rotation = rotation |
|
self.large = large |
|
self.sweep = sweep |
|
self.target_point = target_point |
|
|
|
|
|
|
|
self.angle = radians(rotation) |
|
|
|
|
|
self.center_point = self.theta1 = self.theta2 = self.theta_arc = None |
|
|
|
def _parametrize(self): |
|
|
|
|
|
|
|
|
|
|
|
|
|
rx = fabs(self.rx) |
|
ry = fabs(self.ry) |
|
if not (rx and ry): |
|
return False |
|
|
|
|
|
|
|
if self.target_point == self.current_point: |
|
return False |
|
|
|
mid_point_distance = (self.current_point - self.target_point) * 0.5 |
|
|
|
point_transform = Identity.rotate(-self.angle) |
|
|
|
transformed_mid_point = _map_point(point_transform, mid_point_distance) |
|
square_rx = rx * rx |
|
square_ry = ry * ry |
|
square_x = transformed_mid_point.real * transformed_mid_point.real |
|
square_y = transformed_mid_point.imag * transformed_mid_point.imag |
|
|
|
|
|
|
|
radii_scale = square_x / square_rx + square_y / square_ry |
|
if radii_scale > 1: |
|
rx *= sqrt(radii_scale) |
|
ry *= sqrt(radii_scale) |
|
self.rx, self.ry = rx, ry |
|
|
|
point_transform = Scale(1 / rx, 1 / ry).rotate(-self.angle) |
|
|
|
point1 = _map_point(point_transform, self.current_point) |
|
point2 = _map_point(point_transform, self.target_point) |
|
delta = point2 - point1 |
|
|
|
d = delta.real * delta.real + delta.imag * delta.imag |
|
scale_factor_squared = max(1 / d - 0.25, 0.0) |
|
|
|
scale_factor = sqrt(scale_factor_squared) |
|
if self.sweep == self.large: |
|
scale_factor = -scale_factor |
|
|
|
delta *= scale_factor |
|
center_point = (point1 + point2) * 0.5 |
|
center_point += complex(-delta.imag, delta.real) |
|
point1 -= center_point |
|
point2 -= center_point |
|
|
|
theta1 = atan2(point1.imag, point1.real) |
|
theta2 = atan2(point2.imag, point2.real) |
|
|
|
theta_arc = theta2 - theta1 |
|
if theta_arc < 0 and self.sweep: |
|
theta_arc += TWO_PI |
|
elif theta_arc > 0 and not self.sweep: |
|
theta_arc -= TWO_PI |
|
|
|
self.theta1 = theta1 |
|
self.theta2 = theta1 + theta_arc |
|
self.theta_arc = theta_arc |
|
self.center_point = center_point |
|
|
|
return True |
|
|
|
def _decompose_to_cubic_curves(self): |
|
if self.center_point is None and not self._parametrize(): |
|
return |
|
|
|
point_transform = Identity.rotate(self.angle).scale(self.rx, self.ry) |
|
|
|
|
|
|
|
|
|
num_segments = int(ceil(fabs(self.theta_arc / (PI_OVER_TWO + 0.001)))) |
|
for i in range(num_segments): |
|
start_theta = self.theta1 + i * self.theta_arc / num_segments |
|
end_theta = self.theta1 + (i + 1) * self.theta_arc / num_segments |
|
|
|
t = (4 / 3) * tan(0.25 * (end_theta - start_theta)) |
|
if not isfinite(t): |
|
return |
|
|
|
sin_start_theta = sin(start_theta) |
|
cos_start_theta = cos(start_theta) |
|
sin_end_theta = sin(end_theta) |
|
cos_end_theta = cos(end_theta) |
|
|
|
point1 = complex( |
|
cos_start_theta - t * sin_start_theta, |
|
sin_start_theta + t * cos_start_theta, |
|
) |
|
point1 += self.center_point |
|
target_point = complex(cos_end_theta, sin_end_theta) |
|
target_point += self.center_point |
|
point2 = target_point |
|
point2 += complex(t * sin_end_theta, -t * cos_end_theta) |
|
|
|
point1 = _map_point(point_transform, point1) |
|
point2 = _map_point(point_transform, point2) |
|
target_point = _map_point(point_transform, target_point) |
|
|
|
yield point1, point2, target_point |
|
|
|
def draw(self, pen): |
|
for point1, point2, target_point in self._decompose_to_cubic_curves(): |
|
pen.curveTo( |
|
(point1.real, point1.imag), |
|
(point2.real, point2.imag), |
|
(target_point.real, target_point.imag), |
|
) |
|
|