在Python中实现SVG弧线曲线

9
我正在尝试在Python中实现SVG路径计算,但是在弧线上遇到了问题。
我认为问题出在从终点到中心参数化的转换上,但我找不到问题所在。您可以在SVG规范的第F6.5节中找到有关如何实现它的说明。我也看了其他语言的实现,但也看不出有什么不同。
我的弧形对象实现在这里:
class Arc(object):

    def __init__(self, start, radius, rotation, arc, sweep, end):
        """radius is complex, rotation is in degrees, 
           large and sweep are 1 or 0 (True/False also work)"""

        self.start = start
        self.radius = radius
        self.rotation = rotation
        self.arc = bool(arc)
        self.sweep = bool(sweep)
        self.end = end

        self._parameterize()

    def _parameterize(self):
        # Conversion from endpoint to center parameterization
        # http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes

        cosr = cos(radians(self.rotation))
        sinr = sin(radians(self.rotation))
        dx = (self.start.real - self.end.real) / 2
        dy = (self.start.imag - self.end.imag) / 2
        x1prim = cosr * dx + sinr * dy
        x1prim_sq = x1prim * x1prim
        y1prim = -sinr * dx + cosr * dy
        y1prim_sq = y1prim * y1prim

        rx = self.radius.real
        rx_sq = rx * rx
        ry = self.radius.imag        
        ry_sq = ry * ry

        # Correct out of range radii
        radius_check = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq)
        if radius_check > 1:
            rx *= sqrt(radius_check)
            ry *= sqrt(radius_check)
            rx_sq = rx * rx
            ry_sq = ry * ry

        t1 = rx_sq * y1prim_sq
        t2 = ry_sq * x1prim_sq
        c = sqrt((rx_sq * ry_sq - t1 - t2) / (t1 + t2))
        if self.arc == self.sweep:
            c = -c
        cxprim = c * rx * y1prim / ry
        cyprim = -c * ry * x1prim / rx

        self.center = complex((cosr * cxprim - sinr * cyprim) + 
                              ((self.start.real + self.end.real) / 2),
                              (sinr * cxprim + cosr * cyprim) + 
                              ((self.start.imag + self.end.imag) / 2))

        ux = (x1prim - cxprim) / rx
        uy = (y1prim - cyprim) / ry
        vx = (-x1prim - cxprim) / rx
        vy = (-y1prim - cyprim) / ry
        n = sqrt(ux * ux + uy * uy)
        p = ux
        theta = degrees(acos(p / n))
        if uy > 0:
            theta = -theta
        self.theta = theta % 360

        n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
        p = ux * vx + uy * vy
        if p == 0:
            delta = degrees(acos(0))
        else:
            delta = degrees(acos(p / n))
        if (ux * vy - uy * vx) < 0:
            delta = -delta
        self.delta = delta % 360
        if not self.sweep:
            self.delta -= 360

    def point(self, pos):
        if self.arc == self.sweep:
            angle = radians(self.theta - (self.delta * pos))
        else:
            angle = radians(self.delta + (self.delta * pos))

        x = sin(angle) * self.radius.real + self.center.real
        y = cos(angle) * self.radius.imag + self.center.imag
        return complex(x, y)

你可以使用以下代码进行测试,该代码将使用Turtle模块绘制曲线。(最后的raw_input()只是为了让屏幕在程序退出时不会消失)。
arc1 = Arc(0j, 100+50j, 0, 0, 0, 100+50j)
arc2 = Arc(0j, 100+50j, 0, 1, 0, 100+50j)
arc3 = Arc(0j, 100+50j, 0, 0, 1, 100+50j)
arc4 = Arc(0j, 100+50j, 0, 1, 1, 100+50j)

import turtle
t = turtle.Turtle()
t.penup()
t.goto(0, 0)
t.dot(5, 'red')
t.write('Start')
t.goto(100, 50)
t.dot(5, 'red')
t.write('End')        
t.pencolor = t.color('blue')

for arc in (arc1, arc2, arc3, arc4):
    t.penup()
    p = arc.point(0)
    t.goto(p.real, p.imag)
    t.pendown()
    for x in range(1,101):
        p = arc.point(x*0.01)
        t.goto(p.real, p.imag)

raw_input()

问题:
每个圆弧应该从起点到终点画出。然而,它们是从错误的点绘制的。两条曲线从末端到起点,另外两条曲线从100,-50到0,0而不是从0,0到100, 50。
问题的一部分在于实现说明给出了如何将端点转换为中心点的公式,但没有解释它在几何上的作用,因此我对每个步骤的作用并不清楚。对此的解释也会有所帮助。
2个回答

4

我认为我在你的代码中找到了一些错误:

theta = degrees(acos(p / n))
if uy > 0:
    theta = -theta
self.theta = theta % 360

条件 uy > 0 是错误的,正确的是 uy < 0(如果 uy < 0,则从 (1,0)(ux, uy) 的有向角为负):

theta = degrees(acos(p / n))
if uy < 0:
    theta = -theta
self.theta = theta % 360

那么

if self.arc == self.sweep:
    angle = radians(self.theta - (self.delta * pos))
else:
    angle = radians(self.delta + (self.delta * pos))

这里不需要区分,因为“sweep”和“arc”参数已经在“theta”和“delta”中考虑过了。可以简化为:
angle = radians(self.theta + (self.delta * pos))

最后

x = sin(angle) * self.radius.real + self.center.real
y = cos(angle) * self.radius.imag + self.center.imag

这里混淆了sincos,正确的是

x = cos(angle) * self.radius.real + self.center.real
y = sin(angle) * self.radius.imag + self.center.imag

经过这些修改,程序按预期运行。


编辑:还有一个问题。 point 方法没有考虑可能的 rotation 参数。 以下版本应该是正确的:

def point(self, pos):
    angle = radians(self.theta + (self.delta * pos))
    cosr = cos(radians(self.rotation))
    sinr = sin(radians(self.rotation))

    x = cosr * cos(angle) * self.radius.real - sinr * sin(angle) * self.radius.imag + self.center.real
    y = sinr * cos(angle) * self.radius.real + cosr * sin(angle) * self.radius.imag + self.center.imag
    return complex(x, y)

(参见SVG规范中的公式F.6.3.1。)

感谢指出cos/sin混淆的问题。尽管进行了这些更改,但它仍然无法正确运行,不过现在事情变得更加清晰了一些。 - Lennart Regebro
@LennartRegebro:我已经将修改后的代码放在这里:http://pastebin.com/dp8bYVSq。它似乎可以产生预期的输出。 - Martin R
它确实有问题,我在其他地方也可能搞砸了什么。我太累了,无法弄清楚是什么。谢谢!(第一个只是我测试代码以尝试理解它的剩余部分,但其他错误是真正存在的。我刚刚发现cos / sin混淆的问题,但self.arc == self.sweep是错误的,这需要我再花费数小时才能找出来。) - Lennart Regebro
@LennartRegebro:不用谢。 “rotation”参数还没有生效,我已经更新了我的答案。 - Martin R
好的,谢谢。我知道那个,但现在我也不必自己想出来了。太棒了! - Lennart Regebro
我已经将它制作成了一个模块(https://github.com/regebro/svg.path),如果您提供真实姓名或电子邮件,我愿意在致谢中感谢您的帮助。(您不必这样做。) :-) - Lennart Regebro

3

好主意,所以+1,但我已经用另一个例子进行了测试。我也检查了上面的一个,但没有发现任何新的东西。 - Lennart Regebro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接