"""Draw paths.""" from math import atan2, cos, isclose, pi, radians, sin, tan from ..matrix import Matrix from .utils import normalize, point PATH_LETTERS = 'achlmqstvzACHLMQSTVZ' def _rotate(x, y, angle): """Rotate (x, y) point of given angle around (0, 0).""" return x * cos(angle) - y * sin(angle), y * cos(angle) + x * sin(angle) def path(svg, node, font_size): """Draw path node.""" string = node.get('d', '') for letter in PATH_LETTERS: string = string.replace(letter, f' {letter} ') string = normalize(string) # TODO: get current point current_point = 0, 0 svg.stream.move_to(*current_point) last_letter = None while string: string = string.strip() if string.split(' ', 1)[0] in PATH_LETTERS: letter, string = (f'{string} ').split(' ', 1) if last_letter in (None, 'z', 'Z') and letter not in 'mM': node.vertices.append(current_point) first_path_point = current_point elif letter == 'M': letter = 'L' elif letter == 'm': letter = 'l' if last_letter in (None, 'm', 'M', 'z', 'Z'): first_path_point = None if letter not in (None, 'm', 'M', 'z', 'Z') and ( first_path_point is None): first_path_point = current_point if letter in 'aA': # Elliptic curve # Drawn as an approximation using Bézier curves x1, y1 = current_point rx, ry, string = point(svg, string, font_size) rotation, string = string.split(' ', 1) rotation = radians(float(rotation)) # The large and sweep values are not always separated from the # following values. These flags can only be 0 or 1, so reading a # single digit suffices. large, string = string[0], string[1:].strip() sweep, string = string[0], string[1:].strip() # Retrieve end point and set remainder (before checking flags) x3, y3, string = point(svg, string, font_size) if letter == 'a': x3 += x1 y3 += y1 # Only allow 0 or 1 for flags large, sweep = int(large), int(sweep) if large not in (0, 1) or sweep not in (0, 1): continue large, sweep = bool(large), bool(sweep) # rx=0 or ry=0 means straight line if not rx or not ry: if string and string[0] not in PATH_LETTERS: # As we replace the current operation by l, we must be sure # that the next letter is set to the real current letter (a # or A) in case it’s omitted next_letter = f'{letter} ' else: next_letter = '' string = f'L {x3} {y3} {next_letter}{string}' continue # Cancel the rotation of the second point xe, ye = _rotate(x3 - x1, y3 - y1, -rotation) y_scale = ry / rx ye /= y_scale # Find the angle between the second point and the x axis angle = atan2(ye, xe) # Put the second point onto the x axis xe = (xe ** 2 + ye ** 2) ** .5 ye = 0 # Update the x radius if it is too small rx = max(rx, xe / 2) # Find one circle centre xc = xe / 2 yc = (rx ** 2 - xc ** 2) ** .5 # Choose between the two circles according to flags if large == sweep: yc = -yc # Put the second point and the center back to their positions xe, ye = _rotate(xe, ye, angle) xc, yc = _rotate(xc, yc, angle) # Find the drawing angles angle1 = atan2(-yc, -xc) angle2 = atan2(ye - yc, xe - xc) while angle1 < 0 or angle2 < 0: angle1 += 2 * pi angle2 += 2 * pi # Store the tangent angles node.vertices.append((-angle1, -angle2)) # Fix angles to follow large arc flag if isclose(abs(angle2 - angle1), pi): if sweep and (angle2 < angle1): angle1 -= 2 * pi elif not sweep and (angle2 > angle1): angle2 -= 2 * pi elif large == (abs(angle2 - angle1) < pi): if angle1 > angle2: angle1 -= 2 * pi else: angle2 -= 2 * pi # Split arc into 3 Bézier curves when larger than pi if large: step = (angle2 - angle1) / 3 angles = ( (angle1, angle1 + step), (angle1 + step, angle1 + 2 * step), (angle1 + 2 * step, angle2)) else: angles = ((angle1, angle2),) # Draw Bézier curves matrix = Matrix( cos(rotation), sin(rotation), -sin(rotation) * y_scale, cos(rotation) * y_scale, x1, y1) h = 4 / 3 * tan((angles[0][1] - angles[0][0]) / 4) for angle1, angle2 in angles: point1 = matrix.transform_point( xc + rx * cos(angle1) - h * rx * sin(angle1), yc + rx * sin(angle1) + h * rx * cos(angle1)) point2 = matrix.transform_point( xc + rx * cos(angle2) + h * rx * sin(angle2), yc + rx * sin(angle2) - h * rx * cos(angle2)) point3 = matrix.transform_point( xc + rx * cos(angle2), yc + rx * sin(angle2)) svg.stream.curve_to(*point1, *point2, *point3) current_point = x3, y3 elif letter in 'cC': # Curve x1, y1, string = point(svg, string, font_size) x2, y2, string = point(svg, string, font_size) x3, y3, string = point(svg, string, font_size) if letter == 'c': x, y = current_point x1 += x x2 += x x3 += x y1 += y y2 += y y3 += y node.vertices.append(( atan2(y1 - y2, x1 - x2), atan2(y3 - y2, x3 - x2))) svg.stream.curve_to(x1, y1, x2, y2, x3, y3) current_point = x3, y3 elif letter in 'hH': # Horizontal line x, string = (f'{string} ').split(' ', 1) old_x, old_y = current_point x, _ = svg.point(x, 0, font_size) if letter == 'h': x += old_x angle = 0 if x > old_x else pi node.vertices.append((pi - angle, angle)) svg.stream.line_to(x, old_y) current_point = x, old_y elif letter in 'lL': # Straight line x, y, string = point(svg, string, font_size) old_x, old_y = current_point if letter == 'l': x += old_x y += old_y angle = atan2(y - old_y, x - old_x) node.vertices.append((pi - angle, angle)) svg.stream.line_to(x, y) current_point = x, y elif letter in 'mM': # Current point move x, y, string = point(svg, string, font_size) if last_letter and last_letter not in 'zZ': node.vertices.append(None) if letter == 'm': x += current_point[0] y += current_point[1] svg.stream.move_to(x, y) current_point = x, y elif letter in 'qQtT': # Quadratic curve x1, y1 = current_point if letter in 'qQ': x2, y2, string = point(svg, string, font_size) else: if last_letter not in 'QqTt': x2, y2, x3, y3 = x, y, x, y x2 = x1 + x3 - x2 y2 = y1 + y3 - y2 x3, y3, string = point(svg, string, font_size) if letter == 'q': x2 += x1 y2 += y1 if letter in 'qt': x3 += x1 y3 += y1 xq1 = x2 * 2 / 3 + x1 / 3 yq1 = y2 * 2 / 3 + y1 / 3 xq2 = x2 * 2 / 3 + x3 / 3 yq2 = y2 * 2 / 3 + y3 / 3 svg.stream.curve_to(xq1, yq1, xq2, yq2, x3, y3) node.vertices.append((0, 0)) current_point = x3, y3 elif letter in 'sS': # Smooth curve x, y = current_point x1 = x3 + (x3 - x2) if last_letter in 'csCS' else x y1 = y3 + (y3 - y2) if last_letter in 'csCS' else y x2, y2, string = point(svg, string, font_size) x3, y3, string = point(svg, string, font_size) if letter == 's': x2 += x x3 += x y2 += y y3 += y node.vertices.append(( atan2(y1 - y2, x1 - x2), atan2(y3 - y2, x3 - x2))) svg.stream.curve_to(x1, y1, x2, y2, x3, y3) current_point = x3, y3 elif letter in 'vV': # Vertical line y, string = (f'{string} ').split(' ', 1) old_x, old_y = current_point _, y = svg.point(0, y, font_size) if letter == 'v': y += old_y angle = pi / 2 if y > old_y else -pi / 2 node.vertices.append((pi - angle, angle)) svg.stream.line_to(old_x, y) current_point = old_x, y elif letter in 'zZ' and first_path_point: # End of path node.vertices.append(None) svg.stream.close() current_point = first_path_point if letter not in 'zZ': node.vertices.append(current_point) string = string.strip() last_letter = letter