Converting an svg arc to lines
Asked Answered
L

2

3

I am trying to convert an SVG arc to a series of line segments. The background is, that I want to draw an arc using (reportlab)[http://www.reportlab.com/].

The svg gives me these parameters (accoring to here).

rx,ry,x-axis-rotation,large-arc-flag,sweep-flag,dx,dy

Now I need to determine lines following this arcs. But I do not understand how I can convert this to something geometrical more usable.

How would I determine the center of the ellipse arc and its rotation?

Lyonnaise answered 8/1, 2017 at 21:11 Comment(1)
B
6

SVG elliptic arcs are really tricky and took me a while to implement it (even following the SVG specs). I ended up with something like this in C++:

//---------------------------------------------------------------------------
class svg_usek  // virtual class for svg_line types
    {
public:
    int pat;                // svg::pat[] index
    virtual void reset(){};
    virtual double getl (double mx,double my){ return 1.0; };
    virtual double getdt(double dl,double mx,double my){ return 0.1; };
    virtual void getpnt(double &x,double &y,double t){};
    virtual void compute(){};
    virtual void getcfg(AnsiString &nam,AnsiString &dtp,AnsiString &val){};
    virtual void setcfg(AnsiString &nam,AnsiString &dtp,AnsiString &val,int &an,int &ad,int &av){};
    };
//---------------------------------------------------------------------------
class svg_ela:public svg_usek       // sweep = 0 arc goes from line p0->p1 CW
    {                               // sweep = 1 arc goes from line p0->p1 CCW
public:                             // larc is unused if |da|=PI
    double x0,y0,x1,y1,a,b,alfa; int sweep,larc;
    double sx,sy,a0,a1,da,ang;      // sx,sy rotated center by ang
    double cx,cy;                   // real center
    void reset() { x0=0; y0=0; x1=0; y1=0; a=0; b=0; alfa=0; sweep=false; larc=false; compute(); }
    double getl (double mx,double my);
//  double getdt(double dl,double mx,double my);
    double getdt(double dl,double mx,double my) { int n; double dt; dt=divide(dl,getl(mx,my)); n=floor(divide(1.0,dt)); if (n<1) n=1; return divide(1.0,n); }
    void getpnt(double &x,double &y,double t);
    void compute();
    void getcfg(AnsiString &nam,AnsiString &dtp,AnsiString &val);
    void setcfg(AnsiString &nam,AnsiString &dtp,AnsiString &val,int &an,int &ad,int &av);
    svg_ela()       {}
    svg_ela(svg_ela& a) { *this=a; }
    ~svg_ela()  {}
    svg_ela* operator = (const svg_ela *a) { *this=*a; return this; }
    //svg_ela* operator = (const svg_ela &a) { ...copy... return this; }
    };
//---------------------------------------------------------------------------
void svg_ela::getpnt(double &x,double &y,double t)
    {
    double c,s,xx,yy;
    t=a0+(da*t);
    xx=sx+a*cos(t);
    yy=sy+b*sin(t);
    c=cos(-ang);
    s=sin(-ang);
    x=xx*c-yy*s;
    y=xx*s+yy*c;
    }
//---------------------------------------------------------------------------
void svg_ela::compute()
    {
    double  ax,ay,bx,by;            // body
    double  vx,vy,l,db;
    int     _sweep;
    double  c,s,e;

    ang=pi-alfa;
    _sweep=sweep;
    if (larc) _sweep=!_sweep;

    e=divide(a,b);
    c=cos(ang);
    s=sin(ang);
    ax=x0*c-y0*s;
    ay=x0*s+y0*c;
    bx=x1*c-y1*s;
    by=x1*s+y1*c;

    ay*=e;                  // transform to circle
    by*=e;

    sx=0.5*(ax+bx);         // mid point between A,B
    sy=0.5*(ay+by);
    vx=(ay-by);
    vy=(bx-ax);
    l=divide(a*a,(vx*vx)+(vy*vy))-0.25;
    if (l<0) l=0;
    l=sqrt(l);
    vx*=l;
    vy*=l;

    if (_sweep)
        {
        sx+=vx;
        sy+=vy;
        }
    else{
        sx-=vx;
        sy-=vy;
        }

    a0=atanxy(ax-sx,ay-sy);
    a1=atanxy(bx-sx,by-sy);
//  ay=divide(ay,e);
//  by=divide(by,e);
    sy=divide(sy,e);


    da=a1-a0;
    if (fabs(fabs(da)-pi)<=_acc_zero_ang)       // half arc is without larc and sweep is not working instead change a0,a1
        {
        db=(0.5*(a0+a1))-atanxy(bx-ax,by-ay);
        while (db<-pi) db+=pi2;     // db<0 CCW ... sweep=1
        while (db>+pi) db-=pi2;     // db>0  CW ... sweep=0
        _sweep=0;
        if ((db<0.0)&&(!sweep)) _sweep=1;
        if ((db>0.0)&&( sweep)) _sweep=1;
        if (_sweep)
            {
//          a=0; b=0;
            if (da>=0.0) a1-=pi2;
            if (da< 0.0) a0-=pi2;
            }
        }
    else if (larc)              // big arc
        {
        if ((da< pi)&&(da>=0.0)) a1-=pi2;
        if ((da>-pi)&&(da< 0.0)) a0-=pi2;
        }
    else{                       // small arc
        if (da>+pi) a1-=pi2;
        if (da<-pi) a0-=pi2;
        }
    da=a1-a0;

    // realny stred
    c=cos(+ang);
    s=sin(+ang);
    cx=sx*c-sy*s;
    cy=sx*s+sy*c;
    }
//---------------------------------------------------------------------------

The atanxy(x,y) is the same as atan2(y,x). You can ignore class svg_usek. Usage of svg_ela is simple first feed the SVG parameters to it:

  • x0,y0 is start point (from previous <path> element)
  • x1,y1 is endpoint (x0+dx,y0+dy)
  • a,b are as yours rx,ry
  • alfa rotation angle [rad] so you need to convert from degrees...
  • sweep,larc are as yours.

And then call svg_ela::compute(); that will compute all variables needed for interpolation. When this initialization is done then to obtain any point from the arc just call svg_ela::getpnt(x,y,t); where x,y is the returned coordinate and t=<0,1> is input parameter. All the other methods are not important for you. To render your ARC just do this:

svg_ela arc; // your initialized arc here
int e; double x,y,t;
arc.getpnt(x,y,0.0);
Canvas->MoveTo(x,y);
for (e=1,t=0.0;e;t+=0.02)
 {
 if (t>=1.0) { t=1.0; e=0; }
 arc.getpnt(x,y,t);
 Canvas->LineTo(x,y);
 }

Do not forget that SVG <g> and <path> can have transform matrices so you should apply them after each svg_ela::getpnt(x,y,t) call.

If you are interested how the stuff works compute() simply:

  1. rotates the space so the ellipse semi-axises are axis aligned.

  2. scale the space so ellipse becomes circle.

  3. compute center point for circle

    center lies on line that is perpendicular to line (x0,y0),(x1,y1) and also lies on its midpoint. The distance is computed by Pytagoras and direction from sweep and larc combination.

  4. scale back to ellipse

  5. rotate back

Now we have real center position so also compute the real endpoint angles relative to it. Now for each point on ellipse it is enough to compute it by standard parametric equation of ellipse and rotate to desired position which is what getpnt(x,y,t) does.

Hope it helps a bit.

Here related QA:

with some images explaining the math behind SVG arcs (using the same variable names as here)

Beaman answered 9/1, 2017 at 9:17 Comment(2)
@Lyonnaise glad to be of help ... you would not believe how much time it took to code my SVG decoder/encoder until I got it right for mine purposes ... but that was years ago ... no access to internet etc ... just the specs pdf and many samples (and no 100% working viewer for reference at that time). btw just spotted untranslated comment ... body means pointsBeaman
Oh, I believe that that was a lot of work! Thanks that I now can benefit from your work.Lyonnaise
V
1

For my Java SVG application I needed a conversion of path arc to lines. I used the above code and converted it into a Java class and performed some cleanup.

package de.berndbock.tinysvg.helper;

/**
 * Breaks down SVG arcs into line segments.
 * 
 * @author Bernd Bock <[email protected]>
 */
public class ArcSegmenter {
    
    private static final double PI2 = Math.PI * 2;
    private static final double ACC_ZERO_ANG = 0.000001 * Math.PI / 180.0;
    
    private final double x0;
    private final double y0;
    private final double x1;
    private final double y1;
    private final double a;
    private final double b;
    private final double alfa;
    private final boolean sweep;
    private final boolean larc;
    private double sx, sy, a0, a1, da, ang;  // sx, sy rotated center by ang
//    private double cx, cy;                   // real center
    
    public ArcSegmenter(double x0, double y0, double x1, double y1 , double a, double b, double alfa, int sweep, int larc) {
        this.x0 = x0;
        this.y0 = y0;
        this.x1 = x1;
        this.y1 = y1;
        this.a = a;
        this.b = b;
        this.alfa = alfa;
        this.sweep = sweep != 0;
        this.larc = larc != 0;
        compute();
    }
    
    private void compute() {
        double  ax, ay, bx, by;            // body
        double  vx, vy, l, db;
        boolean _sweep;
        double  c, s, e;

        ang = Math.PI - alfa;
        _sweep = sweep;
        if (larc) {
            _sweep = !_sweep;
        }

        e = a / b;
        c = Math.cos(ang);
        s = Math.sin(ang);
        ax = x0 * c - y0 * s;
        ay = x0 * s + y0 * c;
        bx = x1 * c - y1 * s;
        by = x1 * s + y1 * c;

        ay *= e;                  // transform to circle
        by *= e;

        sx = 0.5 * (ax + bx);     // mid point between A,B
        sy = 0.5 * (ay + by);
        vx = (ay - by);
        vy = (bx - ax);
        l = a * a / (vx * vx + vy * vy) - 0.25;
        if (l < 0) {
            l = 0;
        }
        l = Math.sqrt(l);
        vx *= l;
        vy *= l;

        if (_sweep) {
            sx += vx;
            sy += vy;
        }
        else {
            sx -= vx;
            sy -= vy;
        }

        a0 = Math.atan2(ay - sy, ax - sx);
        a1 = Math.atan2(by - sy, bx - sx);
        sy = sy / e;

        da = a1 - a0;
        if (Math.abs(Math.abs(da) - Math.PI) <= ACC_ZERO_ANG) {     // half arc is without larc and sweep is not working instead change a0,a1
            db = (0.5 * (a0 + a1)) - Math.atan2(by - ay, bx - ax);
            while (db < -Math.PI) {
                db += PI2;     // db<0 CCW ... sweep=1
            }
            while (db >  Math.PI) {
                db -= PI2;     // db>0  CW ... sweep=0
            }
            _sweep = false;
            if ((db < 0.0) && (!sweep)) {
                _sweep = true;
            }
            if ((db > 0.0) && ( sweep)) {
                _sweep = true;
            }
            if (_sweep) {
                if (da >= 0.0) {
                    a1 -= PI2;
                }
                if (da <  0.0) {
                    a0 -= PI2;
                }
            }
        }
        else if (larc) {            // big arc
            if ((da <  Math.PI) && (da >= 0.0)) {
                a1 -= PI2;
            }
            if ((da > -Math.PI) && (da <  0.0)) {
                a0 -= PI2;
            }
        }
        else {                      // small arc
            if (da >  Math.PI) {
                a1 -= PI2;
            }
            if (da < -Math.PI) {
                a0 -= PI2;
            }
        }
        da = a1 - a0;

// center point calculation:
//        c = Math.cos(ang);
//        s = Math.sin(ang);
//        cx = sx * c - sy * s;
//        cy = sx * s + sy * c;
    }
    
    public Point getpnt(double t) {
        Point result = new Point();
        double c, s, x, y;
        
        t = a0 + da * t;
        x = sx + a * Math.cos(t);
        y = sy + b * Math.sin(t);
        c = Math.cos(-ang);
        s = Math.sin(-ang);
        result.x = x * c - y * s;
        result.y = x * s + y * c;
        
        return result;
    }
    
//    public Point getCenterPoint() {
//        return new Point(cx, cy);
//    }
}

If you need the center point, then uncomment the respective lines. Sample code to give you an idea of the usage:

ArcSegmenter segmenter = new ArcSegmenter(currentPoint.x, currentPoint.y, endPoint.x, endPoint.y, rx, ry, phi, sf, lf);
Point p1, p2;
p1 = segmenter.getpnt(0.0);
Line line;

for (double t = increment; t < 1.000001f; t += increment) {
    p2 = segmenter.getpnt(t);
    line = new Line(null, parent, p1.x, p1.y, p2.x, p2.y);
    elements.add(line);
    p1 = p2;
}
Valentia answered 11/4, 2021 at 21:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.