You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
114 lines
3.5 KiB
114 lines
3.5 KiB
import svgpathtools
|
|
import numpy as np
|
|
from math import sin, cos, sqrt, pi, acos, fmod, degrees
|
|
|
|
|
|
# See https://gist.github.com/dov/8d9b0304ba85e3229aabccac3c6468ef/d007910a9b68201a6c95852a220f96711ba3faa1
|
|
def tpl(cplx):
|
|
"""Convert a complex number to a tuple"""
|
|
return (cplx.real, cplx.imag)
|
|
|
|
|
|
def angle_between(u, v):
|
|
"""Find the angle between the vectors u an v"""
|
|
ux, uy = u
|
|
vx, vy = v
|
|
sign = 1 if ux * vy - uy * vx > 0 else -1
|
|
arg = (ux * vx + uy * vy) / (sqrt(ux * ux + uy * uy) * sqrt(vx * vx + vy * vy))
|
|
return sign * acos(arg)
|
|
|
|
|
|
# Implementation of https://www.w3.org/TR/SVG/implnote.html#ArcConversionCenterToEndpoint
|
|
def arc_endpoint_to_center(start, end, flag_a, flag_s, radius, phi):
|
|
"""Convert a endpoint elliptical arc description to a center description"""
|
|
rx, ry = radius.real, radius.imag
|
|
x1, y1 = start.real, start.imag
|
|
x2, y2 = end.real, end.imag
|
|
|
|
cosphi = cos(phi)
|
|
sinphi = sin(phi)
|
|
rx2 = rx * rx
|
|
ry2 = ry * ry
|
|
|
|
# Step 1. Compute x1p,y1p
|
|
x1p, y1p = (
|
|
np.array([[cosphi, sinphi], [-sinphi, cosphi]])
|
|
@ np.array([x1 - x2, y1 - y2])
|
|
* 0.5
|
|
).flatten()
|
|
x1p2 = x1p * x1p
|
|
y1p2 = y1p * y1p
|
|
|
|
# Step 2: Compute (cx', cy')
|
|
cxyp = sqrt(
|
|
(rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2) / (rx2 * y1p2 + ry2 * x1p2)
|
|
) * np.array([rx * y1p / ry, -ry * x1p / rx])
|
|
|
|
if flag_a == flag_s:
|
|
cxyp = -cxyp
|
|
|
|
cxp, cyp = cxyp.flatten()
|
|
|
|
# Step 3: compute (cx,cy) from (cx',cy')
|
|
cx, cy = (
|
|
cosphi * cxp - sinphi * cyp + 0.5 * (x1 + x2),
|
|
sinphi * cxp + cosphi * cyp + 0.5 * (y1 + y2),
|
|
)
|
|
|
|
# Step 4: compute theta1 and deltatheta
|
|
theta1 = angle_between((1, 0), ((x1p - cxp) / rx, (y1p - cyp) / ry))
|
|
delta_theta = fmod(
|
|
angle_between(
|
|
((x1p - cxp) / rx, (y1p - cyp) / ry), ((-x1p - cxp) / rx, (-y1p - cyp) / ry)
|
|
),
|
|
2 * pi,
|
|
)
|
|
|
|
# Choose the right edge according to the flags
|
|
if not flag_s and delta_theta > 0:
|
|
delta_theta -= 2 * pi
|
|
elif flag_s and delta_theta < 0:
|
|
delta_theta += 2 * pi
|
|
|
|
return (cx, cy), theta1, delta_theta
|
|
|
|
|
|
def addSvgPath(self, path):
|
|
"""Add the svg path object to the current workspace"""
|
|
res = self
|
|
path_start = None
|
|
arc_id = 0
|
|
for p in path:
|
|
if path_start is None:
|
|
path_start = p.start
|
|
res = res.moveTo(*tpl(p.start))
|
|
|
|
# Support the four svgpathtools different objects
|
|
if isinstance(p, svgpathtools.CubicBezier):
|
|
coords = (tpl(p.start), tpl(p.control1), tpl(p.control2), tpl(p.end))
|
|
res = res.bezier(coords)
|
|
elif isinstance(p, svgpathtools.QuadraticBezier):
|
|
coords = (tpl(p.start), tpl(p.control), tpl(p.end))
|
|
res = res.bezier(coords)
|
|
pass
|
|
elif isinstance(p, svgpathtools.Arc):
|
|
arc_id += 1
|
|
center, theta1, delta_theta = arc_endpoint_to_center(
|
|
p.start, p.end, p.large_arc, p.sweep, p.radius, p.rotation
|
|
)
|
|
|
|
res = res.ellipseArc(
|
|
x_radius=p.radius.real,
|
|
y_radius=p.radius.imag,
|
|
rotation_angle=degrees(p.rotation),
|
|
angle1=degrees(theta1),
|
|
angle2=degrees(theta1 + delta_theta),
|
|
)
|
|
elif isinstance(p, svgpathtools.Line):
|
|
res = res.lineTo(p.end.real, p.end.imag)
|
|
|
|
if path_start == p.end:
|
|
path_start = None
|
|
res = res.close()
|
|
|
|
return res
|
|
|