194 строки
5.1 KiB
Python
194 строки
5.1 KiB
Python
import math
|
|
### Python conversion of the javascript a2c function
|
|
### Original function at: https://github.com/fontello/svgpath
|
|
|
|
# Convert an arc to a sequence of cubic bezier curves
|
|
#
|
|
|
|
TAU = math.pi * 2
|
|
|
|
# eslint-disable space-infix-ops
|
|
|
|
# Calculate an angle between two unit vectors
|
|
#
|
|
# Since we measure angle between radii of circular arcs,
|
|
# we can use simplified math (without length normalization)
|
|
#
|
|
def unit_vector_angle(ux, uy, vx, vy):
|
|
if(ux * vy - uy * vx < 0):
|
|
sign = -1
|
|
else:
|
|
sign = 1
|
|
|
|
dot = ux * vx + uy * vy
|
|
|
|
# Add this to work with arbitrary vectors:
|
|
# dot /= math.sqrt(ux * ux + uy * uy) * math.sqrt(vx * vx + vy * vy)
|
|
|
|
# rounding errors, e.g. -1.0000000000000002 can screw up this
|
|
if (dot > 1.0):
|
|
dot = 1.0
|
|
|
|
if (dot < -1.0):
|
|
dot = -1.0
|
|
|
|
return sign * math.acos(dot)
|
|
|
|
|
|
# Convert from endpoint to center parameterization,
|
|
# see http:#www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
|
#
|
|
# Return [cx, cy, theta1, delta_theta]
|
|
#
|
|
def get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi):
|
|
# Step 1.
|
|
#
|
|
# Moving an ellipse so origin will be the middlepoint between our two
|
|
# points. After that, rotate it to line up ellipse axes with coordinate
|
|
# axes.
|
|
#
|
|
x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2
|
|
y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2
|
|
|
|
rx_sq = rx * rx
|
|
ry_sq = ry * ry
|
|
x1p_sq = x1p * x1p
|
|
y1p_sq = y1p * y1p
|
|
|
|
# Step 2.
|
|
#
|
|
# Compute coordinates of the centre of this ellipse (cx', cy')
|
|
# in the new coordinate system.
|
|
#
|
|
radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq)
|
|
|
|
if (radicant < 0):
|
|
# due to rounding errors it might be e.g. -1.3877787807814457e-17
|
|
radicant = 0
|
|
|
|
radicant /= (rx_sq * y1p_sq) + (ry_sq * x1p_sq)
|
|
factor = 1
|
|
if(fa == fs):# Migration Note: note ===
|
|
factor = -1
|
|
radicant = math.sqrt(radicant) * factor #(fa === fs ? -1 : 1)
|
|
|
|
cxp = radicant * rx/ry * y1p
|
|
cyp = radicant * -ry/rx * x1p
|
|
|
|
# Step 3.
|
|
#
|
|
# Transform back to get centre coordinates (cx, cy) in the original
|
|
# coordinate system.
|
|
#
|
|
cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2
|
|
cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2
|
|
|
|
# Step 4.
|
|
#
|
|
# Compute angles (theta1, delta_theta).
|
|
#
|
|
v1x = (x1p - cxp) / rx
|
|
v1y = (y1p - cyp) / ry
|
|
v2x = (-x1p - cxp) / rx
|
|
v2y = (-y1p - cyp) / ry
|
|
|
|
theta1 = unit_vector_angle(1, 0, v1x, v1y)
|
|
delta_theta = unit_vector_angle(v1x, v1y, v2x, v2y)
|
|
|
|
if (fs == 0 and delta_theta > 0):#Migration Note: note ===
|
|
delta_theta -= TAU
|
|
|
|
if (fs == 1 and delta_theta < 0):#Migration Note: note ===
|
|
delta_theta += TAU
|
|
|
|
return [ cx, cy, theta1, delta_theta ]
|
|
|
|
#
|
|
# Approximate one unit arc segment with bezier curves,
|
|
# see http:#math.stackexchange.com/questions/873224
|
|
#
|
|
def approximate_unit_arc(theta1, delta_theta):
|
|
alpha = 4.0/3 * math.tan(delta_theta/4)
|
|
|
|
x1 = math.cos(theta1)
|
|
y1 = math.sin(theta1)
|
|
x2 = math.cos(theta1 + delta_theta)
|
|
y2 = math.sin(theta1 + delta_theta)
|
|
|
|
return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ]
|
|
|
|
def a2c(x1, y1, x2, y2, fa, fs, rx, ry, phi):
|
|
sin_phi = math.sin(phi * TAU / 360)
|
|
cos_phi = math.cos(phi * TAU / 360)
|
|
|
|
# Make sure radii are valid
|
|
#
|
|
x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2
|
|
y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2
|
|
|
|
if (x1p == 0 and y1p == 0): # Migration Note: note ===
|
|
# we're asked to draw line to itself
|
|
return []
|
|
|
|
if (rx == 0 or ry == 0): # Migration Note: note ===
|
|
# one of the radii is zero
|
|
return []
|
|
|
|
# Compensate out-of-range radii
|
|
#
|
|
rx = abs(rx)
|
|
ry = abs(ry)
|
|
|
|
lmbd = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry)
|
|
if (lmbd > 1):
|
|
rx *= math.sqrt(lmbd)
|
|
ry *= math.sqrt(lmbd)
|
|
|
|
|
|
# Get center parameters (cx, cy, theta1, delta_theta)
|
|
#
|
|
cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi)
|
|
|
|
result = []
|
|
theta1 = cc[2]
|
|
delta_theta = cc[3]
|
|
|
|
# Split an arc to multiple segments, so each segment
|
|
# will be less than 90
|
|
#
|
|
segments = int(max(math.ceil(abs(delta_theta) / (TAU / 4)), 1))
|
|
delta_theta /= segments
|
|
|
|
for i in range(0, segments):
|
|
result.append(approximate_unit_arc(theta1, delta_theta))
|
|
|
|
theta1 += delta_theta
|
|
|
|
# We have a bezier approximation of a unit circle,
|
|
# now need to transform back to the original ellipse
|
|
#
|
|
return getMappedList(result, rx, ry, sin_phi, cos_phi, cc)
|
|
|
|
def getMappedList(result, rx, ry, sin_phi, cos_phi, cc):
|
|
mappedList = []
|
|
for elem in result:
|
|
curve = []
|
|
for i in range(0, len(elem), 2):
|
|
x = elem[i + 0]
|
|
y = elem[i + 1]
|
|
|
|
# scale
|
|
x *= rx
|
|
y *= ry
|
|
|
|
# rotate
|
|
xp = cos_phi*x - sin_phi*y
|
|
yp = sin_phi*x + cos_phi*y
|
|
|
|
# translate
|
|
elem[i + 0] = xp + cc[0]
|
|
elem[i + 1] = yp + cc[1]
|
|
curve.append(complex(elem[i + 0], elem[i + 1]))
|
|
mappedList.append(curve)
|
|
return mappedList
|