From 7271cdfe80e16cbbdcb09014086386f150c583e8 Mon Sep 17 00:00:00 2001 From: Ryan Oldenburg Date: Fri, 22 Jan 2021 08:47:54 -0600 Subject: [PATCH] arc discretize --- src/pixie/paths.nim | 210 ++++++++++--------- tests/images/paths/pathBottomArc.png | Bin 1083 -> 1092 bytes tests/images/paths/pathCornerArc.png | Bin 975 -> 990 bytes tests/images/paths/pathHeart.png | Bin 1990 -> 1997 bytes tests/images/paths/pathInvertedCornerArc.png | Bin 981 -> 990 bytes tests/images/paths/pathRotatedArc.png | Bin 1276 -> 1302 bytes tests/images/paths/pathRoundRect.png | Bin 810 -> 832 bytes 7 files changed, 115 insertions(+), 95 deletions(-) diff --git a/src/pixie/paths.nim b/src/pixie/paths.nim index f63268e..f4d7320 100644 --- a/src/pixie/paths.nim +++ b/src/pixie/paths.nim @@ -188,82 +188,6 @@ proc `$`*(path: Path): string = if i != path.commands.len - 1 or j != command.numbers.len - 1: result.add " " -## See https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes - -type ArcParams = object - radii: Vec2 - rotMat: Mat3 - center: Vec2 - theta, delta: float32 - -proc endpointToCenterArcParams( - at, radii: Vec2, rotation: float32, large, sweep: bool, to: Vec2 -): ArcParams = - var - radii = vec2(abs(radii.x), abs(radii.y)) - radiiSq = vec2(radii.x * radii.x, radii.y * radii.y) - - let - radians = rotation / 180 * PI - d = vec2((at.x - to.x) / 2.0, (at.y - to.y) / 2.0) - p = vec2( - cos(radians) * d.x + sin(radians) * d.y, - -sin(radians) * d.x + cos(radians) * d.y - ) - pSq = vec2(p.x * p.x, p.y * p.y) - - let cr = pSq.x / radiiSq.x + pSq.y / radiiSq.y - if cr > 1: - radii *= sqrt(cr) - radiiSq = vec2(radii.x * radii.x, radii.y * radii.y) - - let - dq = radiiSq.x * pSq.y + radiiSq.y * pSq.x - pq = (radiiSq.x * radiiSq.y - dq) / dq - - var q = sqrt(max(0, pq)) - if large == sweep: - q = -q - - proc svgAngle(u, v: Vec2): float32 = - let - dot = dot(u,v) - len = length(u) * length(v) - result = arccos(clamp(dot / len, -1, 1)) - if (u.x * v.y - u.y * v.x) < 0: - result = -result - - let - cp = vec2(q * radii.x * p.y / radii.y, -q * radii.y * p.x / radii.x) - center = vec2( - cos(radians) * cp.x - sin(radians) * cp.y + (at.x + to.x) / 2, - sin(radians) * cp.x + cos(radians) * cp.y + (at.y + to.y) / 2 - ) - theta = svgAngle(vec2(1, 0), vec2((p.x-cp.x) / radii.x, (p.y - cp.y) / radii.y)) - - var delta = svgAngle( - vec2((p.x - cp.x) / radii.x, (p.y - cp.y) / radii.y), - vec2((-p.x - cp.x) / radii.x, (-p.y - cp.y) / radii.y) - ) - delta = delta mod (PI * 2) - - if not sweep: - delta -= 2 * PI - - # Normalize the delta - while delta > PI * 2: - delta -= PI * 2 - while delta < -PI * 2: - delta += PI * 2 - - ArcParams( - radii: radii, - rotMat: rotationMat3(-radians), - center: center, - theta: theta, - delta: delta - ) - proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] = ## Converts SVG-like commands to simpler polygon @@ -338,6 +262,114 @@ proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] = discretize(1, 1) + proc drawArc( + at, radii: Vec2, + rotation: float32, + large, sweep: bool, + to: Vec2 + ) = + type ArcParams = object + radii: Vec2 + rotMat: Mat3 + center: Vec2 + theta, delta: float32 + + proc endpointToCenterArcParams( + at, radii: Vec2, rotation: float32, large, sweep: bool, to: Vec2 + ): ArcParams = + var + radii = vec2(abs(radii.x), abs(radii.y)) + radiiSq = vec2(radii.x * radii.x, radii.y * radii.y) + + let + radians = rotation / 180 * PI + d = vec2((at.x - to.x) / 2.0, (at.y - to.y) / 2.0) + p = vec2( + cos(radians) * d.x + sin(radians) * d.y, + -sin(radians) * d.x + cos(radians) * d.y + ) + pSq = vec2(p.x * p.x, p.y * p.y) + + let cr = pSq.x / radiiSq.x + pSq.y / radiiSq.y + if cr > 1: + radii *= sqrt(cr) + radiiSq = vec2(radii.x * radii.x, radii.y * radii.y) + + let + dq = radiiSq.x * pSq.y + radiiSq.y * pSq.x + pq = (radiiSq.x * radiiSq.y - dq) / dq + + var q = sqrt(max(0, pq)) + if large == sweep: + q = -q + + proc svgAngle(u, v: Vec2): float32 = + let + dot = dot(u,v) + len = length(u) * length(v) + result = arccos(clamp(dot / len, -1, 1)) + if (u.x * v.y - u.y * v.x) < 0: + result = -result + + let + cp = vec2(q * radii.x * p.y / radii.y, -q * radii.y * p.x / radii.x) + center = vec2( + cos(radians) * cp.x - sin(radians) * cp.y + (at.x + to.x) / 2, + sin(radians) * cp.x + cos(radians) * cp.y + (at.y + to.y) / 2 + ) + theta = svgAngle(vec2(1, 0), vec2((p.x-cp.x) / radii.x, (p.y - cp.y) / radii.y)) + + var delta = svgAngle( + vec2((p.x - cp.x) / radii.x, (p.y - cp.y) / radii.y), + vec2((-p.x - cp.x) / radii.x, (-p.y - cp.y) / radii.y) + ) + delta = delta mod (PI * 2) + + if not sweep: + delta -= 2 * PI + + # Normalize the delta + while delta > PI * 2: + delta -= PI * 2 + while delta < -PI * 2: + delta += PI * 2 + + ArcParams( + radii: radii, + rotMat: rotationMat3(-radians), + center: center, + theta: theta, + delta: delta + ) + + proc compute(arc: ArcParams, a: float32): Vec2 = + result = vec2(cos(a) * arc.radii.x, sin(a) * arc.radii.y) + result = arc.rotMat * result + arc.center + + proc discretize(arc: ArcParams, steps: int) = + let + initialShapeLen = polygon.len + step = arc.delta / steps.float32 + var prev = at + for i in 1 .. steps: + let + aPrev = arc.theta + step * (i - 1).float32 + a = arc.theta + step * i.float32 + next = arc.compute(a) + halfway = arc.compute(aPrev + (a - aPrev) / 2) + midpoint = (prev + next) / 2 + error = (midpoint - halfway).length + if error >= 0.25: + # Error too large, try again with doubled precision + polygon.setLen(initialShapeLen) + discretize(arc, steps * 2) + return + drawLine(prev, next) + prev = next + + let arc = endpointToCenterArcParams(at, radii, rotation, large, sweep, to) + discretize(arc, 4) + for command in commands: case command.kind of Move: @@ -403,25 +435,13 @@ proc commandsToPolygons*(commands: seq[PathCommand]): seq[seq[Vec2]] = of Arc: let - arc = endpointToCenterArcParams( - at, - vec2(command.numbers[0], command.numbers[1]), - command.numbers[2], - command.numbers[3] == 1, - command.numbers[4] == 1, - vec2(command.numbers[5], command.numbers[6]), - ) - steps = int(abs(arc.delta) / PI * 180 / 5) - step = arc.delta / steps.float32 - var a = arc.theta - for i in 0 .. steps: - polygon.add( - arc.rotMat * vec2( - cos(a) * arc.radii.x, sin(a) * arc.radii.y - ) + arc.center - ) - a += step - at = polygon[^1] + radii = vec2(command.numbers[0], command.numbers[1]) + rotation = command.numbers[2] + large = command.numbers[3] == 1 + sweep = command.numbers[4] == 1 + to = vec2(command.numbers[5], command.numbers[6]) + drawArc(at, radii, rotation, large, sweep, to) + at = to of Close: assert command.numbers.len == 0 diff --git a/tests/images/paths/pathBottomArc.png b/tests/images/paths/pathBottomArc.png index f535a0e74fe3e53de7090c8e3de211644ce168aa..7b96411268976e8c54f6530a9e0c09ef6ae8af84 100644 GIT binary patch literal 1092 zcmeAS@N?(olHy`uVBq!ia0vp^DIm8NO6lgwRF5y7U`8B+zG-af-Bn0VgDta@MdUpa{xJ3oJ)TlxI?xieusT4FAe z+>3fx1CKRnBs)&osBkJmAXTTsXz&Ryi<;2Qzwc9Z!9Lz|`+wP6>=7xh+PkE7tHQyb z%NwP=+GHA^&ba(=Qt1^QBlbH{Mo&0uWQ(nWHucSNlw&NL#$(Ae|JWSADQgy|P59k- zSJk=iz|9$#UxJhu@qY+7IYIeD-IYY~4*pl%1pxqRktr#=eK?zhyPx^8{h1o=ks zM%|^%{AXMf>d(A%dU+?b`NV>QKiM0sz1ZbuZ1%mr&-m<_(B_T@d*0NZIIofXCFD$I z<*e3FekEr1=jDvn%Oq~eN}>&<+=M$U!h`Tj}YqaH|E+A?kGwiYk`m3nLI3J#@a z_UBKFUH0wmKe%yOws=}(N0)+<#eNPOZj+*_11bGk`DZ-$3HbT?3Pd^-{9}Kxab@_z z)BBcXUtXiUl2a+kFP`oBde(Q7FEf{HczMfweM(-aqf5a*o&x=X<#XgF^f%NRp8xYG zVzFL_j)p~j)~9Qwmv2q^Rm=S*b643{FZWP!mm_oj@tiwt?DsEpPvEwzj-AB^7W-cR z7gO}`YfkKYrmaWjoMV69&$zqccJ|qgeZ~joEz7QtdR}z&3Y(zea_)mazc;*Yyt~PF z;^70rKy4c*8n5I7x~l%@96$Ew;SKGLZrj4QaJ|XjRTh_a`-SBd_LoQJh&6v!Z;Wo( z{p;=QHzEetOWqz2e_h8e40K6duca*ixzo&>n0NSY%Wq<_xmWVmapuFUd-oSKeM$1u zHe9ZJ@aOr)xd+bvt(1Pl{b1&@?D%QF?>xBu&%c@HQn%&YZcAOCZ^!mAZ)03%U+*US zLF_}#<*n>b?9%qkoylY6;a@f}*>C>QIrn(a$umuB$ZzmI@cn?;1FK)!G23`_{LW3c z-Pw9_^~1L(bLZ}_*?88pOX0sraa6CR?$J5BW?a5_@?pT4ms{&)Q=;ql#e}U>t_}$8 i)jM$^5_@+0!&~w(@ACQBFM`0*fx*+&&t;ucLK6VOrSiT2 literal 1083 zcmeAS@N?(olHy`uVBq!ia0vp^DIm*k&w#{i!Uiswv?ls9L6v~|T?>{|3xIkg*pIngoC*leE1%A^V-QUP3lxzJgnQqD> zc7&~G;k`n!IXCQA-g*`=T_SCRf?C_Jqz7WVZ4=vrPRf_X-oM~j$5Q6|BRiY1LSyQm z+Zs(b78qVJX9_Qstw_1I`qr}tI>!7}Ei)uuRX&K@ZR=PabTa<@CcRdL`wZob)6W_G z;E-XL^Hx1?^6G(O&I5^8@(sV!X2sk;w<iOdV`&ql)Pc;O-cvSu1+N#_)ySDr8{l393=;Z!m0sGl< z*!3cpPk;H^J0bh3nfkAH88dSPCVS26Us%VwLu|(_IpHFa9ecB82WMMOE&)o|&Go7? zZTsc_Kux0N+5U#w(Azdin{W1Hd)l4Y6!MAP@QQoG>qhNq^7j+u8MlY#e*RUp_gzNQ zjF3+&Lq2sIUb)X`&M^D!N8SYWH?g6&-)xh-_W7%uXiB8k&kCNejuADB*RkkD*X8xf zGS6d>TXE~zXVZJ%>!nhG7RPz0%6qHY%RB#7OL#s*_kioS8h+La$%-9Wv;TUp-*_Rp z-dHks!qlLX>cJ=9FPSty_Seckk{^T(_~-D)Y$*CInlszKvAps5n~W8=((Yf{=eO=g z-eDi56JFnB_kUSwH_NL|bLyWGt)D0V@4QjGtv5H@{r0YT^1EilOxXAtYp(k#zSuJV V+TG_iwZKAw!PC{xWt~$(69Bf7?5O|% diff --git a/tests/images/paths/pathCornerArc.png b/tests/images/paths/pathCornerArc.png index 26f348f82ad4ed5780cbe398017663b55aaa81ec..9e0a56ab2acac106046223af29939021738698e8 100644 GIT binary patch literal 990 zcmeAS@N?(olHy`uVBq!ia0vp^DImEToK^c#raO?f=l=*9!?IW!175_T(Wo09P7vsi)3*xSjfRx zsJrqqcf0oe+Ib5FERRf|^m+5ipOa7i zZ6itRsU<;-bZ-_Sg} z;>8V}6B0!o$0vU-XS^A(-{P3F)n*0VqK@5@Ki_AvXI>ulZda<#2@cC6$_L^Pq$b?% z*qv+1y{S>=(d+~EA9C#HnI$>So8Y{M@r3ybTmL&IqGC%XI?wSuSI>XqUGKfRpV0yn zB&r+j8~LA9&1aK$`L>VgKhyeel8@?}`6fBvVYhja{MhzXLh}=Wax?kh=Wf1!@@ zmi6ov>-#(X=GGOl6jqBnW(jXs zaRTjr{G>=AYxg(L{9xY+Pxh?&yzi#j4WTO{Dcd3^#BZ6^1PpDvHH)r_xQM;pA6&v~ zc`fF2e74H-`>gjSIZj=(OyN_yUbJk1yXf7jKo|dxfk`Y1|DnDNOqjEQQ=esMZ#N> lUZpMj*R^aasU|*C%&47s@oi0CI56Kbc)I$ztaD0e0s#8rwjKZg literal 975 zcmeAS@N?(olHy`uVBq!ia0vp^DIm`YxLjq@79Z(v)p#c&B67D&+jXYA zQT-CJPp2|vj=r?J?B{PE`h4#Kao)d6%)E=Pj7HKE$+{s%N4ESs2r zSD4?($@gdGgY*YH1t~|Or#*9gwnFoMl+zxz8rD0u&zJr_*8FDCvOg*x*gyQy(~soj z|0JJvYTLRWb^F6_Bv(w3%{sMxiokx>d#zt{a;hdc`u@~=aJ@Os)f964(Kj<7Q4K4M;7b`N`F4UefY<$%gGb^ zqhe!bt^-QG{d4=g!}p5gO0le(inr`mhl}w>Mdmx+W0@B)??Lw$u@vDQ|c^7Pnr!7o?%MZ0UI~nZ-aM_Ib09^F&4ZJDN?~%iKRHuPG-dKlfHb zRK3Pui+}u`(ld7MuM{wpbLn9heC&9nf-1}uJ-#~&ng6?)PbbaL&+11dCSG$CpPvoP Os0^O2elF{r5}E*OE3|_E diff --git a/tests/images/paths/pathHeart.png b/tests/images/paths/pathHeart.png index 27add450e3661cf2521093cb5cc898a0c84e0bae..60bcb67ccab47d2b8a2b936e89142e019c57b9df 100644 GIT binary patch delta 1983 zcmY*XeLT~P8viXEdr@YFO6V8U7`4mUVJA{M9S2WA8y@s|2xlr-{*P0-_Q4X8896&ORoUEIieaxj8?3PBD>Fp4l)9T@e$z)q(?$yGkrT)@4S zO8?lb_Ex+0hCyw+M_vfHjRde2$S`F`1C9o-c8Mb&ovs;tnY69437Ys8A=1;m(?{YG zB`R`nf1#dX4s!v-gYRLD=|tNWcN>89(C-bMZfW84M61(%fu%H!O@t#vXt4;8yIv2i z$ZWu<@wi!QMRn_@lT52qVD~k=NU1fPrUF+wQ3tq}U^NZUR+v_ouJZ2vbV1H_8hg@gD}Jk4$fz3Ec=vkUM~dz=48{30Hwi^0M)xS`($-8mY(qJns#JQ5R8uR0i_+NXwCGU7GgEcKVvXU# zzS-r1XNIMIy>j_#grDO9N^U9h-Y~}CYE+$2R4+q4$~5bDEEoP{g_b;F?Mol&_6q8P zlO;1V%r^|bape-|UCO?eZVY(<&uph&kWk6s1RKMndAz@T zT(@&!0AKvtSb<0eeT!KSara=&LyKbgiSB;~#b~4)7R~Kl0NkSiO1OO8#%;*1$;%R_ zn(SF-hEVh`VGJ~`GU5%b(&)<^5&2y-+~XGHwF989km;On>UD{$zKs0Hf;ON?YEO^>~NX?aW9C* zIzkz3wz%0TaphuG7co#7BwF!fQE#DnrBx(CPhIMl8VmMsit0B%Pdx1}Bp$&z_|rTY z*v>ERk#bJ(A9ufM3q@K{Jn_F^)LQDqhcz|6cp*Ek+fuZ)SB3oZO95T}&K|>q7UBq{#X|^jE}`!)OfKkjNoT9N1wP6L5H5H*{ZcY zL)TkRZCQ%*sTj>dTD(aFd`!-~;+PlfJa{M9xG#Be6#GDB!p}#6i)2P^`TVMlb!Kc~ z_q8G2-Tw3z>_G@!s`w4+_W3Wp;r5PVM#?ycS1EdgVs)EQ+DK9XR|8v)Jv#SK!C|?3 zF4*Lei-Q_IU_Wa5zrw|ue*)OwH?^$I^&J8sQeNyoTA>L;z?gvNjn_LIzBDd5fEC<3 zF&A_8NQ|Zd!=wLg{uJz_1x|%C;)m~!MS+THD*<=Kz|&Cd`D`ga4mSKVPuF297h+cm z(X8|l|3HZmaC2Lwgt5Z~mT`pt_~6&g?!+`ue~4ZhWeOE<1;k{>?ywB|JO6P|w#$-H z`~NMslQewp3~X7-ICcN4K&9+{@a4qr`bdgGf4>zTO|hYzUss>2tDr~%)qz5_-~KcL zWFjT-sZGqW;u9!v*ld`vDD;fns55W3a#O%5UiNU+FMA)m7obP~1a!vv)_p-4KXz&X OeFXRg`?gTx3;qYyVz7Du delta 1976 zcmZWqc~p{#7XQc%as#qVLw%o0uE{f_%u1i18=`sKT3pd&Tq2j$22m3%R2nPE#4$9q znle*_TqsS`8Z#I*vywJcx{r&De=ML!i>a#8B=X^cg zL$hEXFC+RbJvJATT2}6tAc+TANGd#dv;FH6B=@T0#TI|XSqEpDgJ76Di_o);buJFE zEffPzvEn4bjjT#$kAqCSGWTQGYx>_^_3`aFw|?-gE=d;smkyBzo%Z3Qq7F2xo4cb5 z)V&SRn}Y~3o?!le4Eq19caRJJYdcYFL=;;xp>)p@z#*BP)IteRxBZ}MJa051`Lr(;u;N^okNmw}zFomTV zFuts0d9)4W0|9-HI_mh!`Vwaud>`E{n9B~R@6=}0W}x~UzcK#Vn`s(F6yIsVll0KI z#Z_+`FGB6`{mtE-fE1$}X-)xRI-N)nigdT*9)p6)d}e0&$46ysDsWf5`^ad%U*YYg zgHWS@nF~e83|tpivmMz;to$+%FuU=txky8pFNzoWtv*%Ed5y1Op>u#I^BY@^h=QQM z`FvVZQ#v%5YQBtqmejhW-hJ&f3Hy7@F08!ojq%(1Pfrz%S1 zo!Ax?p-7;HU70?_yo2N=t2x(Lr|CAVn=nfr&>sD%fJZ__uXUOQOaWnN&;<9@!7~@G z$*JXr*?A6GbVLxuQ;R9|o8V4po&HF&n6|nyJ$*{Rz6X4=Mx4=Y#Wz>5(XlF5^BPT9 zhF#<-Y5y!y?89MGimIwb@Av$=^eBvlUPuI1+SN_gnW?>Uman$p1#KNTSLlg`u)yyr z_{ndq33Dv(jj@PfIAtME2j(_;WAy0q6A^TwGKrExrBQO3zjYHXX0hxMG4I%l=hDde zgu>l9_VCGP`j#>RFEVC!OM_aIbdeMS8 zFj90}MgU(PrHtVs>x=W4LXQVFpQls7alrXNgn~NBjmpxFg%JtS4x@a#0P@~lTf>%D zztvhjj(`Upkz2>&dLOl6jI^}sP3!r}6C-vSi&&4FHl{}Ub|8iv7%!*4-x`)Z`J*^b%Lfd?cGQU{w!ESK|h=wzH9&8kkjDjD5l8-HyKH3w!ygnrNs6Uu_NUj--V+ z;2!n|G`*nLQK!f{_DpL~d6^d8y>Cl>JdB~%ig(iw543?QSm<;ba8HjL=+OpBo)z_p zI}Jcjgal?|1suN6SBxYniUuD3-n?>s2k1{gC`}k1%lmI!sk%h2Q*3--PTZjCH*X74 z!PkfuoHq4iK*FmbHdcb*~$TNK*6?6gM|MF{>k_2 z<@uK9g_g5|h=!2!N+r5f_V;4V;fm`3JpO#eG>J zC!`p8l}`&tw1uhdp}4WhGm?=amz(?HK~Vb6Ars@wt%DawEV4$6lrP=+(A_v9K`&2k z$*l_BOMZ3O#YqyplmF724-v-TN^8bKcD7`-R{g&qSpFz(EClW&G%<#y+ zAhGwD$^ddK!~Q6rdt*fZ1zAcJ<*fhly?9+MPZ#zTf9>pGA*cT-lbzckaZ=*TVoRb{ zH+7GxHaK@QQRe`BNSa%DXc1$(t3h(fZJ_JMt8)oAb)w)BfkeUGQ9FOQcyL&p({>hj z)II|`g@nG1hY-apz@NOq>cd*ANpFcwWhZ=)>K4pI$|h@Yo~|W~(zPntJ0qiMYBgzk z1VV&7-U5=b-}YGiWaf;|i<8f$p)?3S_+dBy7CPMosA{P3Pw{J7^ehj;7_>Hfe#B?RDCX3^ E0lqGqC;$Ke diff --git a/tests/images/paths/pathInvertedCornerArc.png b/tests/images/paths/pathInvertedCornerArc.png index 50691b0322a17733227da848d72b02cf63cdbe1c..bad937245e757059e99aeb5d540ba71d67212cde 100644 GIT binary patch delta 858 zcmV-g1Eu`c2i^yeB!8tzL_t(|0qxsM%N#`*hT$9IDWZ5n5K&2OOweouHHc7)inws2 z!E7W73IWMZ5EU&bur`V!ih`md5=aEaLj)IzAR@K%2e@s-A0W7{g;bbNMia-G>6z~L z>pU0Ca;9qPo>O<%>|p|GhI9w&lL(TWzcY8`4+SwIIe&k5?#k~*rqjz*-XVu_ zSAOla5=KlV$l=_TUyQ72FH;F}BzNVAM zrQDSdjjU-eQwfrDSKjKi5=QL$AT`9hfR}SuUNf?$z3loR$@yo1M-;?}8I1RL)y#JseBS_A_J^#yUo`MXlhPWU2q<@{ay4cVPGO*Y)G|XU$d=U*+qLpH zmK{bxwy1{qAn+A%`Le?($d=R)ZwJ~w-5XXJLP55m{SylK0cd-zR-zyqHoM00IS|%) zXa(7z8h_%gz&W_^Ebg)tWJ79*_W?fv2l_muf^0wy@fh$e&^EmFq98rh5Z?sa>GTV( zZLC8<)>K2hE;;`V&<^}IMnTq4Lu|VY+n4u4YutSy1zAD6^qQQ1cXom4mKE;0GzD2g z+nt`A&v5?uqFvM28UNOD@Rl3;Jks{?+9C zXTY;?{)>9F2=gWvq=tA$a{k`r{J!M;vE+P6&Og~UHYeww1KtOY%(miOIlmYdOfE?7 z$|9e+E5FTdC3ofL+?DThS5D@xeA=F{<(q{U#U||f|EW)nlVAfUlTZUH2@5f}J%f)Z k3n`Ov0~V7|0~i*60j7P4$vLHQbN~PV07*qoM6N<$g4#oz!2kdN delta 854 zcmV-c1F8Jp2h|6VB!8SqL_t(|0qxvPs~klbhT&UdH28s{27(`G=|WtI2udJSOpy>o zj6%eq0d=FeP&ckjVS%*@`2bxAi7OF579uJH1$840{sotfD|c>ODTPQK=fE(@$(b`f z)BT=4&jrIQW~QsAZswz>XJ!{8P(r*ex6QvJxGRhzNOJy`+335KS&ACo1VZ^o%QbK$HIF;MxeIsky$+i!YoPP>9t{_Gv z=U)dNR}dqT^MB`oy$WJL3GpFd?X^Qe3`owu1{|(aQ4j-?^YyLnJ#{JyGO)?p-P6D| zbt(!nu;l!^z=8E?D9C_Hh&KSA0XMEsLqP_VoIeS?G(80c8BlWm6JX!;6cl72CB)}} zw`Ye}kO7ns>t}CY)jjjs;T2>6$@!Oox-)ng1?gD{@qcFEYv9%^4Xq$Oo9vYz*=Se= z=}`&s3E<-TUipTiv{ZELr_o8t?^h zY>`bX$bS-*5bJ@aZ-8eP-K>HvNeS^b;3we8E}K-4B`6_2IEnFS*Uc$NMn;9d)_?d1G{} z{u!A|Zku0n+g!?RbFtoG%cs7=uUh@CtQew`Zv!Hea04Nea03kr$W_t1UA>PeSEZA0 g10s`f0}K}Y2ihKi!X|l8E&u=k07*qoM6N<$f}`S(ApigX diff --git a/tests/images/paths/pathRotatedArc.png b/tests/images/paths/pathRotatedArc.png index a4d3a326b41f058c58f77aa83f7fb4b9b9f8e0c1..f197060a316fa47e4b57d77a75fd082d747a9a16 100644 GIT binary patch delta 1194 zcmeyvIgM+AO8s3=7srqa#<#O$PlURPwB4WLbok67dEFIang25rCW`7ND_^p25p|8K zjdT(W5vbUh7+5TNw(K^5ML`$G0A<0*wvf1p7w3{>zqyKktgYMp z|Ju#9?0j>IEz;Aj*`%MJ7ipZXC=*0UidX4K;l``dAD28m2Pjv3%GekT3K|)M=0XlKLN?W$w$%uH{(S zRm9xD&zQrwVNIg`T)qU`hW6Hq)dK5<9<6uk+Ha~;u9cw!bXFu%zTlQt?i9fPcJSx9yT>;0Y))3d7Ob6T< z&b`?A@R#z1g>S4B?@0&!+4j8tm$hnihk!kELh|ypvt{S<X->EJ9l9l=f{N;9;vh4y4<0o=u{)!@LTHY9lc-7H#Lr!bAC)$JUaiD z?mqjXRvT`gV@26>I3Fk~#;F|H?bJ1OE8DrFdw{N}G-?oM(3!FNL*s?ASndaT^@{&& zmi+kZ@b*N~J*So*F%11_SFh=-zjO-i;BOJJf4luvO;O7W-;(UQse2cNpZ@B!C#d88 z+%`oW_YU_Y+YJ+s)rEzIU;8P1?fpc9G!sTjfWU?fRx zU_0RI^e4^ZqepX5l&X8jvz~3tljCn~DK>1lx^>Ea9Tww!Ma84`tv?(YymeVFFo`o< z`1?;*Y}wJzVv9I7rd5{O8i|E5?f|-0BFbysgwqT!a%wAPLS^M=+;r@LWO>a7h_+DbXvF5z{8Krs#bB>SNK#%O|+q`muEyFg( zh)bo;>~7oFmOlKEb-i-En^Q@Y#*tg$yNcwkcez;#1zkDZIQODQ$0BylS8v{P{;GQN z!t3AL36Jirdz7Q&Qf&Wq?~`Ziq%t|)>QDU?*-f{5 zwwfw_?eYHKRF*!Q*QCd<)%5<-`mn6tYb_dzN7p-d`Ty^iTYD-XYO)_oOwbvws`SM7 jrdFFDIrTgw5q;$6Uw8fAnIeB>1|aZs^>bP0l+XkKpE5F^ literal 1276 zcmeAS@N?(olHy`uVBq!ia0vp^DImdAN77wRAAbujqKhJfGYA&;eKOgRLoV-4s1KBw}}PC~53F z)vC}Wbka%KJ5ZobP>E%J@)T>6$dd-av9`6#|5qm@%ro8otRy`A?yk)ueVQ>HhWJsG zxX~Im>6RZ_igDUURE}i7`n}qz%U?4e`1slBk5+>^!=3N;+u2)w zL@L$=bZqDTn5Foye51FPT1o_O7{^C$hHuO}SPxh+EcY!q5zCNZ+aTRIafy0s#bN<{ z!AFY~k4_fea#6aALo%#6mf;@r2HuQ2&u%i9G47B(ko;AkTxZD-@BF&2h9+V__w+MZ zF!a=&xU^!b+R9d*)dy7>;^tgDqcuG{%djf< zr``66dNZOr1YWoP`KQ-3cM|t&#WyJ)A0KPl-8|U1Xb4NDA&8Aa*WUw+wa4(-qvf8>1a(r-(rFvcCl^K!4m@Fr{)*e@FN zXLsGY;-Z#0><<>6Q`SE$y30`Q2>V(crQ#MD_5~}2eZ#*n+CDDl`uG~?sa@|bTyb*h za#q~O+_3J(??b5!dF#$kxxf3(l`Bpniu?E^dJon*UH-Yd<%h%c!{O@$bbWSg^*75h zN!;bz5&!y?(|qj|#sim2)qfp(RrPJD_^z!g47k`># zx7c_+e_H6HS-LKQNpm-6=Z4Lw4o#l1zkLrp?uY}o4b6;$k zb3udoOfG9C$KA!(7rn1l+&cMlmX3?yBYg%Nt_MD~cZyANx9BbT;U@i8N6DpM>~AwK z!(29fwlDmyQHqsIO47SI0$Y9xeeP$M91p8 X`z|rWtqZ6GmTC;1u6{1-oD!M-2rlOg4${ST6C5mxLML&mg9_Tg!9k}YsI`Nj=pZ;a6p0*LX(c!5IZ1`D z&G$e^ayj?hob%>qOAjW+lf3~Ke{>wA4*LQe4{$!f)c|(_gf6-b2G|>5TYy@G&z&@G zJ4hWi2N(y&fFa-@u%nX~vr45kr?sa&xENp{z|#mn+gVRrLF#Z4m<0B>bI~G`jYdySo(drY#ZT6?j95c z32-0SlRLTndE@Sm^WP|0vV+v&2yi`XR$FRUfaeiDExydR_1$ziJVhPg4_W5awfF~y%9zmj#elMalUZ5 z7E**0HJEKN<{#NXk_evy^a6*{b6eI(4aS!pwSPoWkR-qhU?XrKJ-4RDfNSZSrRj(Y z#X-^qB1{H&1`Gn5D(3BGGr&*{Zl%2{-H75Ke?^!La2xmvd|5cYb>Y6-yapaK|MhgC z2JhP4fVP7q5#}PijPNkdzawI#D{tx3koF+$#a!B(c6AiQR-S{1aS$;MBE~_)IEWYr f5tFY01s2S2npjzF?$%$%00000NkvXXu0mjfgLE70 delta 608 zcmV-m0-ycB2C4>-BWMEgNkl9MXMv}{aGi^;b7@Z|0(_6~ru1Hm zgEZj+a1&^hUaXI#y%`JeE5c04eHR63!YJ^_F|r*FFdgAT?u_gpO0(G|7q$ zMYx+Yt0YMJW;K|zsCC#F;d;l}3ME0VRm`_MgAroR(2^h_XF;pxldl1IlfD5Tf6V^~ zlK!qD`=`lmnLiP(E<4jj_67$y32g4N&N{_f`5fVP&SC>OgOUKFz>f6TicLp&oHMH= z$R=PsXHn~LE5cmI*$O2=-UBCqp^9~^yRX1xgx}qVEXfX%77Q>C99f!f8O}FgCSzPn zc90~(Y=9A9S9)$eO*Pn+<;SQuSz$fI7nNB*8v^?Uw|#ZaQn4BH4EGUCK~Xn?jF<*l0=wKVItg* u5c~W{dyw`beetPnRY7d+IgyYQ7W@VM5?5;AHv=R90000