Express SVG arc as series of curves
Asked Answered
N

2

4

I'm trying to accurately express an SVG Path as a UIBezierPath however sadly the addArc on UIBezierPath does not account for ellipses, only circles (only 1 value for radius).

bezierPath.addArc(withCenter:CGPoint radius:CGFloat startAngle:CGFloat endAngle:CGFloat clockwise:Bool)

My thinking would be to break the arc into pieces as svg curves, but I'm not sure how to calculate it.

If I know the shape I want to make I can turn say, the top right corner arc

a150,150 0 1,0 150,-150 into a curve c82.84,0,150,44.77,150,100

but as I'll be parsing any possible arc, I need to know how to break up any ellipse and also calculate control points for each of the Bezier curves.

I've been looking at various resources that show cubic curves calculated in this way... http://www.spaceroots.org/documents/ellipse/node12.html

but I'm not sure how to express this in code

This is what I have so far....

Values for an a path in SVG

radiusX radiusY rotationOfArcX isLarge isSweep destinationX destinationY

Edit

@Spektre your answer looks great when I render out some simple paths but the path is moving depending on large + sweep combination.

For example

Small Sweep / Large No Sweep

M 180.0 80.0 a50,50 0 0,1 50,50 z

M 180.0 80.0 a50,50 0 1,0 50,50 z

X has been translated +100

M 180.0 80.0
M 280.0 80.0
C 280.0 73.62 278.63 66.76 276.19 60.87
C 273.75 54.97 269.87 49.15 265.36 44.64
C 260.85 40.13 255.03 36.25 249.13 33.81
C 243.24 31.37 236.38 30.0 230.0 30.0
z

^^ small sweep example

Small No Sweep / Large Sweep

M 180.0 80.0 a50,50 0 0,0 50,50 z

M 180.0 80.0 a50,50 0 1,1 50,50 z

Y has been translated +100

M 180.0 80.0
M 180.0 180.0
C 186.38 180.0 193.24 178.63 199.13 176.19
C 205.03 173.75 210.85 169.87 215.36 165.36
C 219.87 160.85 223.75 155.03 226.19 149.13
C 228.63 143.24 230.0 136.38 230.0 130.0
C 230.0 123.62 228.63 116.76 226.19 110.87
C 223.75 104.97 219.87 99.15 215.36 94.64
C 210.85 90.13 205.03 86.25 199.13 83.81
C 193.24 81.37 186.38 80.0 180.0 80.0
C 173.62 80.0 166.76 81.37 160.87 83.81
C 154.97 86.25 149.15 90.13 144.64 94.64
C 140.13 99.15 136.25 104.97 133.81 110.87
C 131.37 116.76 130.0 123.62 130.0 130.0
z

^^ large sweep example

My codes version of your arc

M 10 70 a 133.591805 50 12.97728 0 0 70 -50 z

M 10.0 70.0
M 65.33 62.67
C 53.75 67.15 35.85 69.91 17.44 70.06
C -0.97 70.2 -24.36 67.78 -45.14 63.57
C -65.92 59.36 -89.13 52.34 -107.24 44.79
z

My version of your code


private func arcAsCurves(x0: CGFloat, y0: CGFloat, a: CGFloat, b: CGFloat, angle: CGFloat, large: Bool, sweep: Bool, x1: CGFloat, y1: CGFloat) -> String {

        //return "L\(x1) \(y1)"

        var localSweep = sweep
        if large { localSweep = !localSweep }

        let pi = CGFloat.pi
        let pi2 = pi*2
        let ang = pi-(angle*pi/180.0) // [deg] -> [rad] and offset to match my coordinate system

        let e = a/b
        var c = cos(+ang)
        var s = ang == pi ? 0.0 : sin(+ang)

        let ax = x0*c-y0*s // (ax,ay) = unrotated (x0,y0)
        var ay = x0*s+y0*c
        let bx = x1*c-y1*s // (bx,by) = unrotated (x1,y1)
        var by = x1*s+y1*c

        ay *= e // transform ellipse to circle by scaling y axis
        by *= e

        // rotated centre by angle

        let axd = ax+bx
        let ayd = ay+by

        var sx = 0.5 * axd // mid point between A,B
        var sy = 0.5 * ayd

        var vx = ay-by // perpendicular direction vector to AB of size |AB|
        var vy = bx-ax

        var l = (a*a / (vx*vx + vy*vy)) - 0.25 // compute distance of center to (sx,sy) from pythagoras
        //l=divide(a*a,(vx*vx)+(vy*vy))-0.25

        if l < 0 { // handle if start/end points out of range (not on ellipse) center is in mid of the line
            l = 0
        }

        l = sqrt(l)
        vx *= l  // rescale v to distance from id point to center
        vy *= l

        if localSweep { // pick the center side
            sx += vx
            sy += vy
        } else {
            sx -= vx
            sy -= vy
        }

//        sx += localSweep ? vx : -vx
//        sy += localSweep ? vy : -vy

        var a0 = atan2(ax-sx, ay-sy) // compute unrotated angle range
        var a1 = atan2(bx-sx, by-sy)

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

        ay /= e
        by /= e
        sy /= e // scale center back to ellipse

        // pick angle range
        var da = a1-a0
        let zeroAng = 0.000001 * pi/180.0

        if abs(abs(da)-pi) <= zeroAng { // half arc is without larc and sweep is not working instead change a0,a1

            var db = (0.5 * (a0+a1)) - atan2(bx-ax,by-ay)

            while (db < -pi) { db += pi2 } // db<0 CCW ... sweep=1
            while (db > pi) { db -= pi2 } // db>0  CW ... sweep=0

            if (db < 0.0 && !sweep) || (db > 0.0 && sweep) {
                if da >= 0.0 { a1 -= pi2 }
                if da < 0.0 { a0 -= pi2 }
            }
        }
        else if large {
            if da < pi && da >= 0.0 { a1 -= pi2 }
            if da > -pi && da < 0.0 { a0 -= pi2 }
        }
        else {
            if da > pi { a1 -= pi2 }
            if da < -pi { a0 -= pi2 }
        }

        da = a1-a0

        c = cos(-ang)
        s = sin(-ang)

//        var cx = sx*c-sy*s // don't need this
//        var cy = sx*s+sy*c

        var n: Int = 0
        let maxCount: Int = 16

        var dt: CGFloat = 0.0

        var px = [CGFloat]()
        var py = [CGFloat]()

        n = Int(abs((CGFloat(maxCount) * da)/pi2))

        if n < 1 { n = 1 }
        else if n > maxCount { n = maxCount }

        dt = da / CGFloat(n)

        // get n+3 points on ellipse (with edges uniformly outside a0,a1)

        let t = a0 - dt

        for i in 0..<n+3 {

            // point on axis aligned ellipse
            let tt = t + (dt*CGFloat(i))
            let xx = sx+a*cos(tt)
            let yy = sy+b*sin(tt)

            // rotate by ang
            let c: CGFloat = cos(-ang)
            let s: CGFloat = sin(-ang)

            px.append(xx*c-yy*s)
            py.append(xx*s+yy*c)
        }

        let m: CGFloat = 1/6

        var string = ""

        for i in 0..<n
        {
            // convert to interpolation cubic control points to BEZIER
            let x0 = px[i+1];                     let y0 = py[i+1];
            let x1 = px[i+1]-(px[i+0]-px[i+2])*m; let y1 = py[i+1]-(py[i+0]-py[i+2])*m;
            let x2 = px[i+2]+(px[i+1]-px[i+3])*m; let y2 = py[i+2]+(py[i+1]-py[i+3])*m;
            let x3 = px[i+2];                     let y3 = py[i+2];

            if i == 0 {
                let mString = String(format: "M%.2f %.2f", x0, y0)
                string.append(mString)
            }

            let cString = String(format: "C%.2f %.2f %.2f %.2f %.2f %.2f", x1, y1, x2, y2, x3, y3)
            string.append(cString)
        }

        return string
    }
Newspaper answered 11/12, 2019 at 16:33 Comment(2)
I have Delphi code for approximation of ellipse arc(defined by ellipse center, semiaxes, rotation, starting and sweeping angle) with Bezier. Would it be useful for you?Illyricum
Couldn’t hurt. It’s just about how to turn arcs into curves. Once that’s done I can do the restNewspaper
R
0
  1. see Converting an svg arc to lines

    It will compute any point on the SVG elliptic arc by parameter so you can create as many control points as you want.

  2. use interpolation cubics

    take a look at:

    especially the last link from there:

    as it converts the interpolation cubic control points directly to BEZIER cubic control points.

    So divide your arc into n points. Form 4 point cubic patches and convert them to BEZIERs ...

    Beware you need at least 4 cubics for whole ellipse but 8 is better so you do not have too big deviation from original shape. So based on the angular size of the arc decide how many cubics you need 1..8 for 0..360 deg

    Do not forget to handle the edges of the elliptic curve by extrapolating 1st and last control point slightly outside the angle range of the arc so the 1st derivation is not screwed ...

[Edit1] example ...

Let us consider this simple SVG:

<svg width="512" height="512" viewBox="3.621934 13.621934 90.255485 62.818094" fill="none" stroke="none" stroke-width="1px" transform="matrix(1,0,0,1,0,0" >
 <g>
  <path id=" " stroke="magenta" d="M 10 70 a 133.591805 50 12.97728 0 0 70 -50 "/>
 </g>
</svg>

So (no)/unit matrix, single arc path looking like this:

preview

After rendering the precomputed values using:

_test_ellarc(10,70,133.591806,50.0,12.97728,0,0,80,20);

source is below... Will give:

computation

With some added explanations:

labels

(x0,y0) = (10,70)    // last point before 'a'
      a = 133.591805
      b = 50
    ang = 12.97728 deg
  sweep = 0 
   larc = 0 
(x1,y1) = (80,20)    // lower case 'a' means relative coordinates to x0,y0

Now I created simplified C++ example that computes everything and render overlay with GL in my SVG editor engine:

//---------------------------------------------------------------------------
void svg2scr(double *p,double x,double y) // SVG(x,y) -> OpenGL(p[3])
    {
    p[0]=x;
    p[1]=y;
    p[2]=0.0;
    win_SVGEditor->edit.scl2g_svg2ogl.l2g(p,p);
    }
void draw_line(double x0,double y0,double x1,double y1,double r,double g,double b)
    {
    double p0[3],p1[3];
    glBegin(GL_LINES);
    glColor3f(r,g,b);
    svg2scr(p0,x0,y0); glVertex2dv(p0);
    svg2scr(p1,x1,y1); glVertex2dv(p1);
    glEnd();
    }
//---------------------------------------------------------------------------
void _test_ellarc(double x0,double y0,double a,double b,double ang,bool larc,bool sweep,double x1,double y1)
    {
    // ang [deg]
    // x0,y0,x1,y1 are absolute !!!

    // (ignore) init for rendering
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    // -----------------------------------------
    // [SVG elliptic arc to parametric ellipse]
    // -----------------------------------------

//  draw_line(x0,y0,x1,y1,1.0,0.0,0.0); // raw start-end point line (red)

    // precomputed constants
    double sx,sy,a0,a1,da;      // sx,sy rotated center by ang
    double cx,cy;               // real center
    // helper variables
    double  ax,ay,bx,by;
    double  vx,vy,l,db;
    int     _sweep;
    double  c,s,e;

    ang=M_PI-(ang*M_PI/180.0);  // [deg] -> [rad] and offset to match my coordinate system
    _sweep=sweep;
    if (larc) _sweep=!_sweep;

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

    ay*=e;                  // transform ellipse to circle by scaling y axis
    by*=e;

    sx=0.5*(ax+bx);         // mid point between A,B
    sy=0.5*(ay+by);
    vx=(ay-by);             // perpendicular direction vector to AB of size |AB|
    vy=(bx-ax);

/*  pythagoras:
    |v|=|b-a|
    (|v|/2)^2 + l^2 = r^2
    l^2 = r^2 - (|v|/2)^2
    l^2 = r^2 - |v|^2 * 0.25
    l^2/|v|^2 = r^2/|v|^2 - 0.25
*/
    l=divide(a*a,(vx*vx)+(vy*vy))-0.25; // compute distance of center to (sx,sy) from pythagoras
    if (l<0) l=0;           // handle if start/end points out of range (not on ellipse) center is in mid of the line
    l=sqrt(l);
    vx*=l;                  // rescale v to distance from id point to center
    vy*=l;

    // (ignore) perpendicular line going through both centers (dark GREEN)
//  draw_line(sx-vx,sy-vy,sx+vx,sy+vy,0.0,0.3,0.0);

    if (_sweep)             // pick the center side
        {
        sx+=vx;
        sy+=vy;
        }
    else{
        sx-=vx;
        sy-=vy;
        }

    a0=atanxy(ax-sx,ay-sy); // compute unrotated angle range
    a1=atanxy(bx-sx,by-sy);
/*
    // (ignore) unrotated scaled to circle center and start-end points (GREEN)
    draw_line(ax,ay,bx,by,0.0,0.7,0.0);
    draw_line(ax,ay,sx,sy,0.0,0.7,0.0);
    draw_line(bx,by,sx,sy,0.0,0.7,0.0);
    // (ignore) unrotated scaled to circle circle arc a0..a1 (GREEN)
    glBegin(GL_LINE_STRIP);
    glColor3f(0.0,0.7,0.0);
    for (double aaa=a0,daa=(a1-a0)*0.05,p[3],i=0;i<=20;aaa+=daa,i++)
     { svg2scr(p,sx+a*cos(aaa),sy+a*sin(aaa)); glVertex2dv(p); }
    glEnd();
*/
    ay=divide(ay,e);
    by=divide(by,e);
    sy=divide(sy,e);        // scale center back to ellipse
/*
    // (ignore) unrotated ellipse center and start-end points (BLUE)
    draw_line(ax,ay,bx,by,0.0,0.0,0.7);
    draw_line(ax,ay,sx,sy,0.0,0.0,0.7);
    draw_line(bx,by,sx,sy,0.0,0.0,0.7);
    // (ignore) unrotated ellipse arc a0..a1 (BLUE)
    glBegin(GL_LINE_STRIP);
    glColor3f(0.0,0.0,0.7);
    for (double aaa=a0,daa=(a1-a0)*0.05,p[3],i=0;i<=20;aaa+=daa,i++)
     { svg2scr(p,sx+a*cos(aaa),sy+b*sin(aaa)); glVertex2dv(p); }
    glEnd();
*/
    // pick angle range
    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;

    // rotated center
    c=cos(-ang);
    s=sin(-ang);
    cx=sx*c-sy*s;
    cy=sx*s+sy*c;
/*
    // (ignore) rotated center and start-end point (RED)
    draw_line(x0,y0,x1,y1,1.0,0.0,0.0);
    draw_line(x0,y0,cx,cy,1.0,0.0,0.0);
    draw_line(x1,y1,cx,cy,1.0,0.0,0.0);
*/
    // -----------------------------------------
    // [parametric ellipse to BEZIER cubics]
    // -----------------------------------------

    int i,n;
    const int N=16;         // cubics per whole ellipse
    double t,dt;
    double px[N+3],py[N+3]; // all interpolation cubics control points
    double w=2.5; // rendered cross size
    // arclength 0..2*PI -> cubics count 1..8
    n=fabs(double(N)*da)/(2.0*M_PI);
    if (n<1) n=1;
    if (n>N) n=N;
    dt=da/double(n);
    // get n+3 points on ellipse (with edges uniformly outside a0,a1)
    for (t=a0-dt,i=0;i<n+3;i++,t+=dt)
        {
        double c,s,xx,yy;
        // point on axis aligned ellipse
        xx=sx+a*cos(t);
        yy=sy+b*sin(t);
        // rotate by ang
        c=cos(-ang);
        s=sin(-ang);
        px[i]=xx*c-yy*s;
        py[i]=xx*s+yy*c;
        // render
        draw_line(px[i]-w,py[i]+w,px[i]+w,py[i]-w,0.5,0.2,0.7);
        draw_line(px[i]-w,py[i]-w,px[i]+w,py[i]+w,0.5,0.2,0.7);
        }
    // process cubics
    AnsiString txt="";
    for (i=0;i<n;i++)
        {
        const double m=1.0/6.0;
        double x0,y0,x1,y1,x2,y2,x3,y3;
        // convert to interpolation cubic control points to BEZIER
        x0 = px[i+1];                     y0 = py[i+1];
        x1 = px[i+1]-(px[i+0]-px[i+2])*m; y1 = py[i+1]-(py[i+0]-py[i+2])*m;
        x2 = px[i+2]+(px[i+1]-px[i+3])*m; y2 = py[i+2]+(py[i+1]-py[i+3])*m;
        x3 = px[i+2];                     y3 = py[i+2];
        // render
        if (!i) txt+=AnsiString().sprintf("M%.6lf %.6lf",x0,y0);
                txt+=AnsiString().sprintf(" C%.6lf %.6lf %.6lf %.6lf %.6lf %.6lf",x1,y1,x2,y2,x3,y3);
        }
    // here save the txt into your SVG path

    // (ignore) exit from rendering
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
    }
//---------------------------------------------------------------------------

where svg2scr converts from SVG units into my GL view coordinates and draw_line render debug output so you can ignore them. The _acc_zero_ang=0.000001*M_PI/180.0 is just accuracy constant. The unimportant stuff is taged with (ignore) comment and can be deleted.

  1. Now magenta is the SVG rendered elliptic arc.

  2. The start end point is unrotated by angle (blue line not going to center).

    That makes the ellipse axis aligned so scaling its y axis by a/b will turn it into circle with radius a (red line not going to center). From its mid point is cast a perpendicular line (which side depends on sweep/larc). Which must hit the circle center along the way somewhere.

  3. The circle center/midpoint/start or end point form a right angle triangle so using Pythagoras I compute the distance from mid point to center. Converted to scale 'l' of the vx,vy vector.

  4. Once you got the center unrotated circle sx,sy you can compute edge angles a0,a1 of the arc using atan2

  5. Now scale back to ellipse by scaling y axis by b/a (blue)

  6. Now rotate the (sx,sy) center back by ang getting (cx,cy) is all you need (red)

Now we can finally obtain any point on the ellipse so we can convert to BEZIER cubics. Here overlay of original ellipse (magenta) and new BEZIER (red) paths.

BEZIER

Beware they do not match precisely here zoom:

accuracy loss

  1. decide how many (n) cubics are needed based on |a1-a0|

    looks like 16 BEZIER cubics per 360 deg is sort of enough. The more the higher precision... In this case resulting n=3 cubics

  2. obtain n+3 interpolation cubic control points

    each cubic needs 4 points but it renders curve between second and third one so there will be 2 points left over. That means we need to obtaine them slightly outside a0,a1 range so the shape will not be distorted. The control points are simply the points on the ellipse (crosses)...

  3. for each interpolation cubic create BEZIER counterpart

    simply use the formula from link above to transfom between the two cubics.

  4. save new SVG.

    I did just use txt string variable that hold the new path and added it to test svg manualy.

Here the merged paths:

<svg width="512" height="512" viewBox="3.621934 13.621934 90.255485 62.818094" fill="none" stroke="none" stroke-width="1px" transform="matrix(1,0,0,1,0,0" >
 <g stroke="blue">
  <path id=" " stroke="magenta" d="M 10 70 a 133.591805 50 12.97728 0 0 70 -50 "/>
  <path id=" " stroke="red"     d="M10.000000 70.000000 C24.500960 70.325512 38.696601 69.272793 49.846109 67.045096 C60.995616 64.817400 70.632828 61.108261 76.897046 56.633820 C83.161264 52.159379 86.914255 46.304086 87.431414 40.198450 C87.948573 34.092813 85.301045 26.896880 80.000000 20.000000 "/>
 </g>
</svg>
Rhomboid answered 12/12, 2019 at 8:14 Comment(16)
Being someone who doesn't understand this topic, having this is pretty useless in the end. I'd really like some way to understand what is happening, with variable names that are useful for understanding the parsingNewspaper
I'm 90% sure my problem is I'm calculating the control points incorrectly if I can fix that it's sorted, but I'm also sure there's a more elegant solution than mine.Newspaper
My arcs break into pieces easily but the curves are not always the right shape.Newspaper
@Newspaper I added [Edit1] with example (finaly finished editing took me a while ...) Also I cleared out obsolete comments ... you should tooRhomboid
Wow @Spektre, amazing answer.. let me go through it and see if I can understandNewspaper
I've almost got it... Just to confirm, pi2 is 2*pi right? not pi*piNewspaper
@Newspaper yes its 2*M_PI = 6.283185307179586476925286766559 You should go step by step (render and confirm if the same output then mine and only then go to next step) If you try all at once its 99.999% probability of something going wrong messing up the whole thingRhomboid
I've got it working on simple arc tests but having trouble on complex paths, but thanks for the advice :)Newspaper
The main thing I don't understand is why it performs the move first... I've added it to my question.Newspaper
@Newspaper because elliptic arc start and endpoint might not belong to the ellipse at all. In such case the curve starts elsewhere (closer to the center which lies on the midpoint between start and end point in the circle scale).Rhomboid
ultimately my version of your code doesn’t give the correct results. I can’t take it step by step because I don’t know what the values should be at each stage. So I don’t know where it’s incorrect.Newspaper
If you wouldn’t mind printing the logs for each stage of your code I could debug from that.Newspaper
I tried your code in C++ and it outputs huge numbers M-9123.863516 -3298.674400 C-9123.555791 -3298.887556 -9123.254158 -3299.103008 -9122.958756 -3299.320657 even after I found your atanxy formula on a different post. i'm not sure what to say at this point... i feel like you know what you're talking about but there's too many gaps for me to fill and I don't know how to fill them.Newspaper
I converted your atanxy formula and now i get the correct shape... still doesn't work for complex paths thoughNewspaper
Probably worth putting atanxy in there as I just assumed it wasn't bespoke.. and if you'd bothered to read my conversation you'd have seen that mistake.Newspaper
@Newspaper atanxy(x,y) = atan2(y,x) ... from math but that one has usually some problems near singularities (0/90/180/270 deg) ... the stages outputs points so just visually compare my and your output (blue, green and then red lines) first the start-end point line in each color then center point (form a triangle with previous line) and lastly the arc ... on images of mine is the scale present so you can easily check the values... (those draw_line statement in my code)... If you can not render you can add it to the SVG with M L commandsRhomboid
U
0

https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes

  1. Find cx, cy, θ1 and Δθ of the arc of ellipse described in section B2.4 of the implementation notes above.
  2. Then get bezier representation of the arc of the unit circle using Δθ.
  3. Rotate the arc by θ1.
  4. Expand the arc with rx, ry,
  5. Rotate right the arc by φ.
  6. Translate the arc by [ cx, cy ]

Sample implementation of the bezier representation of arc of unit circle.

//  'CB' stands for Cubic Bezier
const
UnitArcByCB = θ => {    // θ: -2π < Angle < 2π
    const L = 4 / 3 * Math.tan( θ / 4 )
    const sinθ = Math.sin( θ )
    const cosθ = Math.cos( θ )
    return [
        [ 1, 0 ]
    ,   [ 1, L ]
    ,   [ cosθ + sinθ * L, sinθ - cosθ * L ]
    ,   [ cosθ, sinθ ]
    ]
}
Uphold answered 30/1, 2023 at 9:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.