Implementing SVG arc curves in Python
Asked Answered
L

2

9

I'm trying to implement SVG path calculations in Python, but I'm running into problems with Arc curves.

I think the problem is in the conversion from end point to center parameterization, but I can't find the problem. You can find notes on how to implement it in section F6.5 of the SVG specifications. I've also looked at implementations in other languages and I can't see what they do different either.

My Arc object implementation is here:

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)

You can test this with the following code that will draw the curves with the Turtle module. (The raw_input() at the end is just to that the screen doesn't disappear when the program exits).

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()

The issue:

Each of these four arcs drawn should draw from the Start point to the End point. However, they are drawn from the wrong points. Two curves go from end to start, and two goes from 100,-50 to 0,0 instead of from 0,0 to 100, 50.

Part of the problem is that the implementation notes give you the formula from how to do the conversion form endpoints to center, but doesn't explain what it does geometrically, so I'm not all clear on what each step does. An explanation of that would also be helpful.

Lenna answered 18/1, 2013 at 12:52 Comment(0)
S
4

I think I have found some errors in your code:

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

The condition uy > 0 is wrong, correct is uy < 0 (the directed angle from (1, 0) to (ux, uy) is negative if uy < 0):

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

Then

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

The distinction is not necessary here, the sweep and arc parameters are already accounted for in theta and delta. This can be simplified to:

angle = radians(self.theta + (self.delta * pos))

And finally

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

Here sin and cos are mixed up, correct is

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

After these modifications, the program runs as expected.


EDIT: There is one more problem. The point method does not account for a possible rotation parameter. The following version should be correct:

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)

(See formula F.6.3.1 in the SVG specification.)

Standin answered 18/1, 2013 at 22:3 Comment(6)
Thanks for the cos/sin mixup. Even with these changes it doesn't run correctly though, but things make a but more sense now.Lenna
@LennartRegebro: I have put the modified code here: pastebin.com/dp8bYVSq. It seems to produce the expected output.Standin
It does, I must have fudged something else on the way as well. Too tired with this to figure out what. Thanks! (The first was just a leftover from me testing things to try to understand the code, but the other errors were real ones. I'd just figured out the cos/sin mixup, but that the self.arc == self.sweep was wrong would have taken me hours more to figure out.Lenna
@LennartRegebro: You are welcome. The "rotation" parameter did not yet work, I have update my answer.Standin
OK, thanks for that. I knew that, but now I don't have to figure out that part myself either. Awesome!Lenna
I've made a module out of it ( github.com/regebro/svg.path ) and like to credit you with your help, if you give me a real name or email. (You don't have to. ) :-)Lenna
C
3

Maybe you could have a look at links below, there seems to be a step-by-step guide on how to compute the arcs (see computeArc()):

http://svn.apache.org/repos/asf/xmlgraphics/batik/branches/svg11/sources/org/apache/batik/ext/awt/geom/ExtendedGeneralPath.java

or

http://java.net/projects/svgsalamander/sources/svn/content/trunk/svg-core/src/main/java/com/kitfox/svg/pathcmd/Arc.java

Covarrubias answered 18/1, 2013 at 21:44 Comment(1)
Good idea, so +1, but I'd already gone through it with another example. I checked one of the above as well, but didn't find anything new.Lenna

© 2022 - 2024 — McMap. All rights reserved.