From 775149a07293d32d8599de58bce361f6d541ad74 Mon Sep 17 00:00:00 2001 From: JonRC Date: Tue, 10 Feb 2026 12:51:32 -0300 Subject: [PATCH] fix: render elbow arrows with sharp corners instead of curves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elbow arrows (elbowed: true) were rendered using roughjs rc.curve() which produces smooth Bézier curves. Now uses rc.path()/gen.path() with an SVG path of straight lines and small quadratic corners (radius 16px), matching the original Excalidraw rendering. - Port generateElbowArrowShape() from excalidraw to src/shared.ts - Add elbow check in renderLine() (PNG) and svgLine() (SVG) - Add visual regression fixture with L/Z/U elbow shapes --- docs/VISUAL-REGRESSION-TESTING.md | 1 + src/export-svg/renderers.ts | 7 ++ src/export.ts | 7 ++ src/shared.ts | 74 ++++++++++++++ tests/visual/baselines/elbow-arrows--svg.png | Bin 0 -> 10318 bytes tests/visual/baselines/elbow-arrows.png | Bin 0 -> 15084 bytes tests/visual/fixtures/elbow-arrows.excalidraw | 93 ++++++++++++++++++ 7 files changed, 182 insertions(+) create mode 100644 tests/visual/baselines/elbow-arrows--svg.png create mode 100644 tests/visual/baselines/elbow-arrows.png create mode 100644 tests/visual/fixtures/elbow-arrows.excalidraw diff --git a/docs/VISUAL-REGRESSION-TESTING.md b/docs/VISUAL-REGRESSION-TESTING.md index 260e8b1..b6c38e2 100644 --- a/docs/VISUAL-REGRESSION-TESTING.md +++ b/docs/VISUAL-REGRESSION-TESTING.md @@ -108,6 +108,7 @@ The test runner automatically creates a dark mode variant for any fixture named | `opacity` | Elements at 100%, 60%, 30% opacity; semi-transparent text | | `all-fonts` | All 7 supported font families (Excalifont, Nunito, Lilita One, Comic Shanns, Virgil, Cascadia, Liberation Sans) | | `colored-arrows` | Elbow/curved/straight arrows with non-transparent backgroundColor; verifies arrow paths are not filled | +| `elbow-arrows` | Elbow arrows (elbowed: true) with L/Z/U shapes rendered as straight segments with rounded corners; includes regular curved arrow for regression | | `combine-horizontal` | Combine command: horizontal layout of basic-shapes + arrows-lines | | `combine-vertical` | Combine command: vertical layout of basic-shapes + arrows-lines | | `combine-labels` | Combine command: horizontal layout with --labels flag | diff --git a/src/export-svg/renderers.ts b/src/export-svg/renderers.ts index d361db6..02fcc40 100644 --- a/src/export-svg/renderers.ts +++ b/src/export-svg/renderers.ts @@ -8,6 +8,7 @@ import { escapeXml, FONT_FAMILY, FRAME_STYLE, + generateElbowArrowShape, getCornerRadius, getRoughOptions, isPathALoop, @@ -119,6 +120,12 @@ function svgLine( ([px, py]) => [x + px, y + py] as [number, number], ); + if (element.elbowed) { + const pathStr = generateElbowArrowShape(transformed, 16); + const drawable = gen.path(pathStr, options); + return svgPathsToMarkup(gen.toPaths(drawable), options.strokeLineDash); + } + const drawable = transformed.length === 2 ? gen.line( diff --git a/src/export.ts b/src/export.ts index f5d66bb..cd94a9c 100644 --- a/src/export.ts +++ b/src/export.ts @@ -19,6 +19,7 @@ import { type ColorTransform, FONT_FAMILY, FRAME_STYLE, + generateElbowArrowShape, getCornerRadius, getRoughOptions, isPathALoop, @@ -125,6 +126,12 @@ function renderLine( ([px, py]) => [x + px, y + py] as [number, number], ); + if (element.elbowed) { + const pathStr = generateElbowArrowShape(transformedPoints, 16); + rc.path(pathStr, options); + return; + } + if (transformedPoints.length === 2) { rc.line( transformedPoints[0][0], diff --git a/src/shared.ts b/src/shared.ts index 0cc50fb..07a0a03 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -497,6 +497,80 @@ export function prepareExport( }; } +// --------------------------------------------------------------------------- +// Elbow arrow shape generation +// --------------------------------------------------------------------------- +// Ported from excalidraw/packages/element/src/shape.ts +// Generates SVG path for elbow arrows with straight segments and small rounded corners. + +function pointDistance(a: [number, number], b: [number, number]): number { + return Math.hypot(a[0] - b[0], a[1] - b[1]); +} + +export function generateElbowArrowShape( + points: [number, number][], + radius: number, +): string { + const subpoints: [number, number][] = []; + + for (let i = 1; i < points.length - 1; i++) { + const prev = points[i - 1]; + const next = points[i + 1]; + const point = points[i]; + + const prevIsHorizontal = + Math.abs(point[1] - prev[1]) < Math.abs(point[0] - prev[0]); + const nextIsHorizontal = + Math.abs(next[1] - point[1]) < Math.abs(next[0] - point[0]); + + const corner = Math.min( + radius, + pointDistance(point, next) / 2, + pointDistance(point, prev) / 2, + ); + + // Approach subpoint (coming from prev) + if (prevIsHorizontal) { + subpoints.push([ + point[0] + (prev[0] < point[0] ? -corner : corner), + point[1], + ]); + } else { + subpoints.push([ + point[0], + point[1] + (prev[1] < point[1] ? -corner : corner), + ]); + } + + // Corner control point + subpoints.push([point[0], point[1]]); + + // Departure subpoint (going to next) + if (nextIsHorizontal) { + subpoints.push([ + point[0] + (next[0] < point[0] ? -corner : corner), + point[1], + ]); + } else { + subpoints.push([ + point[0], + point[1] + (next[1] < point[1] ? -corner : corner), + ]); + } + } + + const d = [`M ${points[0][0]} ${points[0][1]}`]; + for (let i = 0; i < subpoints.length; i += 3) { + d.push(`L ${subpoints[i][0]} ${subpoints[i][1]}`); + d.push( + `Q ${subpoints[i + 1][0]} ${subpoints[i + 1][1]}, ${subpoints[i + 2][0]} ${subpoints[i + 2][1]}`, + ); + } + d.push(`L ${points[points.length - 1][0]} ${points[points.length - 1][1]}`); + + return d.join(" "); +} + /** Escape XML special characters for SVG attribute values */ export function escapeXml(str: string): string { return str diff --git a/tests/visual/baselines/elbow-arrows--svg.png b/tests/visual/baselines/elbow-arrows--svg.png new file mode 100644 index 0000000000000000000000000000000000000000..7a6b128c9ed0c49faab195a8eb623f5da5a993d5 GIT binary patch literal 10318 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 literal 0 HcmV?d00001 diff --git a/tests/visual/baselines/elbow-arrows.png b/tests/visual/baselines/elbow-arrows.png new file mode 100644 index 0000000000000000000000000000000000000000..2c67fb8239152e86c8823e78af66e87984dc04c8 GIT binary patch literal 15084 zcmd_RWmuG5)IWL;2q-Zs5()ztpwf!O&=|CIw@QP+ASECrk0Kx-tsvb)IW!C@4fd0 z_C+*Y$J6T{@^2%R~OzBTg_TdQ|JG=GzNO!mDhx*Af%;KU|WOOuBx07#OA{!eUok(d} z8J+>#95h-kGBT1P9_z9;Mr;f|qS0up2cAf}qeu6AJLc3hHTzu-hkyTmo|cxzur7FT za6l*W_J`Qf@*`?mT6RkROdDLu;#d_nSaA01+P25b8Wb}}ev`G!4eu14u0wH;;M&~M zF|8CkskD ztwyJt!hVdY{rv?DZ0L~`9PMa6Nqf1$xZRd0R-ORCp5-%LJ{R9pWZ9#xsTs9iYFtY( zTxyr!#OVG!Bf+y`G;IPh)Y~d)tP+qd_d%@OA5Y^AQK~tcL>V|u-NechB3QG0?u5!P zCx?Ym%Bms}nwFMXsrw~vql%A>)nFuH7BPvM#+tcs(eRAh#=#SHJ^B%sH!i9im(=P7}+*SzPJWDT-i(iA7*9&n>biKRK zTq(|}UneZp0PDOE@4kvDzm0glwkzHl=$nqVo7;C1u-?rX=$IYN@yhpTN^kL=?GUyd zDYJK`g&`xy*9NlltJ!3Ai>)FL_tx_HZH5Q+so~6{(RqQvVoElTwsyK^SxBX=;S<@x z>PtaRYgW>dq3!qQIrAIVtZOGHs0pt-))CSETVf=gqM{JX-!>b7y3e6gvHTA#=Ha?5 z18y_3tfQ*(O?6(ZS0I11ZD9s7@!M?_L~LqelIgy;ifJOF;XqEMOoj?I6(Qw%$P#*g ztUjG;j9$}EOK885w8!uHTFq2q>*)kUzSNhkwLD!=z{6=+IoN20DkB*4Zrr|+Bs+H^ zTYh>%@_Qwht-sA~B&~@?yyt~EnfJ$w2=AAo?z^AeD;*b9W7+CL%O)}l`5ZdcPZC#T zF__F`H`}4-qw9y)S90zbV1yDEKy3QhR};8ATx@NFz95Z$>MShkzQKG_*%Vn&boMbg zu+%dhPZkTV`UhBViCxI?$DeJDw!K@jFR$H= z@b1B{Ch)0!yk-_S%PO9dcGqa%TWaq{5^u*T_A0kFR;GS7xOY)>X_*~v@#-Q%f^@`X z+yj#pDSo;U!ms6PQed*MxcYRvRGvvIdgT4!enl0%V_W6;8zI&omwgCje4-8AWF;l} zu-5^{<_>%hlH5=2-8?6#D3G(qgW9f69~wC-l%~hM)RSa>H7-I&SCQUS7yarnAs%Am zuz3Gj4t7qpy#xnrV}hvEH_IECjcBd*fqWyT+_;(7aUb0A%RfEaWg?fHKvn-VK3Yh7 z+~fEr_L{M=e2GU=!td(GOm>q9nzCzv7MKuXRqacU;h9789lEhCI@g?-N5alpt9=S* z)p5CnC>8>eF9a9hE}O3J=*E_G70TBi7q@5X@akioc1u1?tJ855mOr`agcrgUUoozl|p_dTzS1FJoZv^q|Ls)wrDtABb^0Sjc6n2^Zcrg7W+dBj)Iaq(a`ZU|HT zHYG3aOpj9=`)h2 zwXJ`M-E2HFq=W-gE_Cw%PkFCm*4rw>SoX(iYHweQw_Qg{-YXK4!mqq~R_zOnfSgORRWQxRXSiX_*;pa}HcBYIcmi`{7>*o}bM{$AX$rE^4((Idkbs^=L|Vf2C)B&-0i{ z&%En3UX?H8i2PcOU3n`9iVwYL6;zLM_`^~cj7|fw?a4#uGW8=I@5@?ox0p@LaI%el z#N78fuF9%i;m~A>CzhQMj<-1&0ZH&P!<*6}@m?W#vstM2cGjd?_(+s8Bot@{Vkscs zJqsGwn8cj$rl1OfZCvpLEMEZxB-Zels7i8RSEFS0B|obk6QbiLgtYWVkelCS-o~0aDg9yH}B4UNaQn z7KQ@Pa;$5-$~ko0TxZ^mmc1YchyJ+&{LczY@6Bq-nBeI$927HrcB{Dkb(&HMSC5j} zcp^SNxmZe&`w%|JS%pUqE0Vl;k`SCR|KVqbqpn~4utB+&K#hGgqrow`8ixfXJ|H;} zlHiPA>Ms4(bgMY4$6LBi)b@bsBd3*Py`rSIg`;V8Gx&9m$S+pKP0437$L^8S9sQee z*^YhRW4DK6N|(7PxTF9J+4M;54j=VJ+<_xrAWg-~=>>~#MmO4hE0#w6G_InHu)Jkg z@TA%Px{12kSMi5tKH-CsRj2(4i|m$WP1Z{XDGQA*x>$B!9G+3_dKi|d(sR_I|Nf{) zW3JrRev$JUvB9G?GX*2D*46Hb+ZW)qKa?%b;IMvFtzvZC0|_ts^{+xErD__;On)YY zE*r8YOFIpJzyi@Zt51p5*na5k^*1dY{A?Dgn$8MKEv-2T$@}h+67~A%=WNS_BhcQ5 ziwFx+-PAeDmJgRL-ga33!Eo4c;*yq(n9{Q3PxE9VYEmBff*h;4hjbtyi3vE=GY6qE zo$JKUR)3uY;1*LCSJX}5WKS-@dHI}5+vU2wN;&#~elpK+>!EFtb(VPhzKZOm0`}E~LXSrhHOTW+&cvR2{WxMdID%*h$>9o;<-9zQ#m@sS(wW$RWV zD$7m&POb`fCK0BtX^{eY;nb1gOa1TT!@zzuen1XU;45n!No75iswPr?U)6F5g0g0$onp;pRlI= z(%9633%_5^q_^6IO3#yp-#FgW<%e>6M9i|W@}&#W`KO8|353aizJ0k1WO%B;Rj?ToUeid@pGA(G$ZEt+2&W*Cl8RT?cIv$ z_j<2uUKExpEIx(aCLX2!$&+y z8rHtC{A2=B{agzxosYrEo^iZ;;v2lgM9)qUWx0);<^xAklWXC$8B~M7UQ&L9wlwR+ zjvYEFj(ays+1jQ9gKGW*P1T%ZCdN)OAp5=0KB>J*ib26w9fmFup6B$(ibsQk0R~1( z8Ngcl`p5vUS?A}%Nk-5hmB$z&bB3%)g^ULq zND$7Ar>*ziWoOR?_ioP=zRrO+12)O05%7u5sS`I>l4vUv5; zmzHdHuuwYSuYi$`1`fgtOwnRVFGv0U4eBteQpRH^bTS{m#>YE=JmG4^ z*>_yzqARMK74=e?FJoh8qx5YczjSt~Yj)8fYhhU4#e<8(9!C>1ijkB_C zdxPGU%Ce1N)~HCyY#TH=32+P5MB53Fv5`DFmfzgwM}(b#X|=2=1P0}Ek7ZiVokX!l z-33ggLJaVm2VBB0y-)ck9y+xzT&K^Ddv$Q4x0lJRZDs6~3CebS7JV@N@A35Z&%})0 zwM>VpM%p~D5U{~V{7W@9Z$WTK3~J1PZq)prZjtK2{nCcmudghhoC6CAM3VBaMmfm* zKR7x73qjw^@Ob^5H7XrQ>YM^TU@`C0X%+i*;ykYlRHIJZE8gs)JTTMjpv1z%hJF;3 z)pPqx_PxW=j4ltUUs~n?rVb1IC!59N4;x-x`%fUnKq2v~XFqh!O9FyO>ic5$yQqS6 z91~j5tu{HG9(Zydq!A2G_wL?X!h=z%$7^L9pfZw!PTu` z{Nx;oE{@39%Y&m9i^rF|wAu?XLVI{@C@}#o&tOFGXjF6*AC{IAUS*MzIE^2LEbQ5h z)>mnR$ON(0Mv!*$e(z7pFhm>|9Xg7z*jDZ)KCpGMgX7ssusf4;6+(Dd>86!-s?mxK zz6kh$8;u19saiWdbfyY+*aiACJe` z-FE+P_@hrKJyf8Gee`5}+0b0+CH$PHls2FAYy(YtP|qan1Tt#1bf2yx4k*lcTX!nuZ3BVo^OC7;MS|73<*Sr|J4A#VQT9hM$sQJDAqApV2 z%X7{Lp?J;|Z5>{X>{s^LEoL}KpGOa>S?GUOF|8C1FMb0M>)iSBN4IkG;#>mp-3Nt8 zK0ExXMa8UVDJt~rFx36yG2WnsM^8eI-+q6y_#6|V5xo@b!q|yF9Q{Cz!w+vV5~H=S z#DjhNfk${316BNd*@kQOc(2ozUEjy4Hhzu%V?1Dd(RIJ`ol`Ssr#P2KavfEx7Ph)e z!@;)ueR1G2YtAoy6yuBZe3eHW|~+w>qESebxuOxQ}hhYPYTyB z{S5#!*y{esLcjC$psv#cLIvK?CO81r@Dkc`q4V0ko_0il*#HnO$n{7JWF%5Gj-7KR z^f)G-e`wn6`3yi*h2+4X_Wui!fQ*ExNXNrwyc;?&j3#HF+AmC3Wa3=yr;S$i0mzeH ztO{bnfiv-`!P^GoU1*wSx|rm!*1KQWbbn%0yrG~Jt&PX2ci-_!-ilVj{KeX^vZnxp zZ;!BqWU49p4Qj7YwNm7VbOmo3@N1fL;~mZs3GK~~9ljM6M+LMmy)hfv&%i}3v<%gQ zwt>RL*jGb_)oKGZVsdKH z$DVFCIa%^&NjREQ*@ik*dUoExbGw6+06Amv z7zDpf$r(mB)Ji3#dsA;AY3Ap`_Z zcMh0YI0bEqN8(IvrLv=8WrYL=s(i>-4&0XGKq9mz5t)N7#S6{*g$KfZfQ9!P03{CH?5pURO_Le zK4zGd?7XZU#3-u!$h`fzRS*;-9-{H6Vbk%EEPKG0I+s2R@( zD{f>_OKiD-;?T{0y=Sp!BjSJDS%B+$3d0N*?iSQ8$lxSZXNe}&veXP{-v_xyIumSi zxRdXdYr%`QpKfF!ry^NGfq)srByQMcp)N3j@~v=OcvWNuwdvv88jC+k_$uf4HnlZh zABX3*IyNlR@6@F$fz1<~nK)!)%3lS8#w1498n_bkb-_2OpaGP@A|Ev$sby%ne&Cxs z`IXN)iiX=Tr$1M3DLvMfN0%KkU)N@&SaZrv-%Yq8&`^MVlP^_mpOMaWfW1xf^`~e` zVrj*U6KF@TxS7}c%`WksGC;JyT3WilJ~U>Qy)3%@rKM%cuFNsffDQDuW03aA%nR+p zYwFIqW=OCr4KPjh&&>z3KheAm;bAIW89bD{5PHfGkM z4qd6lY8*^|QB}Mi zznV&ei*{i*kH*{QZMS#68MQssWm>b9pxjyPllLXLtf`?9$@q4{XYj2tL$Lc=l@<^O z9goQ;Md6jAhEF|;dR@#(6b_%c)n6FJP*1Y zBQF9>yGohnL6k0j-K2%VaFr7ssx*zE$u%r@2gpA60k{yB<8ic$iMtD$iKS1HlznY5 zlPGQcFUeh*Qo)Flu=ugo8@(;3O9Zff?+7$-90(O2)tmN(o>OyM& znDTMZ+`m*9>OMpZ{F4 z$C=bm{$Oi5s}@E_d~2sTog$fY6y|fOQ4^koZ>thve_AE}ug76hN=(SI;vgwTj@1ZN4_w=ata2?rKag8go zU6VTb;~u>jB3{9gb>2Q+Et`h7eSP>e%Ctf81_S@=uZ-{3AA|PV1oJ>!jYh(X@I>p< z2HL>P+Ub{VHe6?=_hClWuz9hDUrq_1@zBbj!b-Qzo;lES;^s#t+!>|sq}QrSM0jl~ zirobj+RAXT{9L?yUO=GrZmGKnqsQ?^lT}Z8P+KGn6^8FjEZd#ta;D*m3M(@of6C+r zU1`t(3Ln_CXvoV4MQxMEX-SV~?ozdvWcMN-|E~Xj)M0VFzY&&teEiPia3Oq2ca z`pzvI&e1$hQ(SSeBNki@^vxF7@ENCpUteRv8;V-8lZQTwDI1w@Hlp#6_Fo)UX?ms94zxu~ZQB<(U|k>`SVJ zYx%C&QBD9*Fz@w<$oRbFq^B!fWDvycU*&U$Qf1-zV0i1_You!p$Z7=oseO-gx?r6jAcvdwkR&LO;PhW23)(X6O z=TCEsoa7~NYkik>0i|AfloxPUQQe~FuB>X~ka>3B*N23L#3qwhq$#0x>Fq}060L3QoS_5R!dsT5V_+pPtEmiiUGc^tJ26laSF3T~R14;&Y+ z8H4z*sjFKcXK{G@s-&F!HBUBOl_ZP%RN^%7Y^!EUSH0q}QX9R4;o|U&rKPYM2%?L` z&jLsWs;iSi65Y()081Q}C?yb!Ke(KAv8LwkI}?*%n+GPQW^!R7LqlOrci69dp}yGr z+0?YbNtUBkHCg;gYAUn+Hw}#dO%0Y~{nWDk^_q0dnwn0Q#e=Ww4$>_HS>fHmf>J(f z9!d)yhxbvN&5pqkq@b%RjEMDvYwT1wHCKGkJ{T^It~C5m@;dZ(D52OuY(hRzd!+E| zuOhCltmrF{_tWU!T9u7SzhNN2+mVqNrZ7$Cl5QdAh{~~;Cd2$3A>0ej5NSsAqUI4PXMxPIc#vsfIs;oe;1a6Y z(fpYeE@PSXWZW|wohGjVPU0hbQ~y#n46h42tili>$=xbO2nzlu)B&kqZc(Io-Ctc@ zCQ11<+g(+C1lirtu&@)R=&1fSL2tZKu}i{Ra*~G2Rj1 zd0Xm?vaWJ+5A5pMs?6L=kO|Wj-%l(kyUGY9UYf9Q@biy|vTD`PVl8ql6yem(e6gy3 zdPD%djxLb(Wam^6tpAuboukvjD5Tzn(q`!p&u@F`hu$_i6Pi~fN6;xbJHKXQDKHO_ zGkjY&8vsEvFa1)gCpKr;VvwP@9{Pfr7Iy06L+b-lQuPPs!6rJ+(H9`pLtM$jeXNVt zk$Hl~sp#G>Tt)Hw)@bV`Zm%o@<0dv$O*1W>;854e4`HdD=?dt>DYDpX7*rNlb#zyh z(Ug`3JrSuDu@`9cOHN*GVWgbR=tpgT!S|b;DbmZ+Ee(M-ITs*rs?qq<7dgM57ndEF znO+!baI{+jPFH`r8X+WH#5>MkYH3eXnu==Il8Q{axd8i zSBL)l96`zeKVJYh!+a!!I-gxZe*Zle&MbKe4A{h8MIwF=(<1SwElW+Hc=AhU$1nW1 zo@NO=eF5kX?{h|q9NQ5?gMkUp;NUTyCjRMrq2Vh_DDC1nV2H1uOrRcsBGNnkJy@bi z|Nrbr6GQ+Q@uU>IuEzHFwW220A_C9n4RL=2NB`st3KFQ+49FtefOl~Zr+*QuUAc&W z)8Nv=qRbpvAgJ`5DRol31OPd{eOxqYAX%Tw$;u@Q6w-g6pYx{<_{(vokY3r<{!3PY z@~>4e2K5DKugJp5sg2~q<-hWv$<4_*a6!KQpChzW(db!1FmZ$@h9uAl2}*uw@N@0t zi((&NICyLe9)o)bC^73M3aOn_Ul&38-`Z6@UOB(N`tg+nw`7pTZNNbzD9a_(9s|jM z)+7`|3P^7tkmj|ry9Gf5fG6#hWEK2z_gnra{POfaI2C zoL1C0w=DYYnMeDScS;1ec4D$@c+o*YYtfwlJt|J_QY0m8E>nr+uF>y$ zp?t$UA-5z%)XYUzLX}7>N4e`?e~iS6_{OthI#FGpcu|`?atV(yP(B6~VlRH3Y;M!w zMQHZjOSr$iQcC*$ZE|;ob1|0VrX@fh_uVHX4nhQ_>cFJY&TuhX+#~5e>;C)eCu^=nQS45t*~? zIAiD%x9NGhMeH{TlM%(ok=@n4{-t~^P;ze0&Skg#bBT0&(bPr9`|6|(uIQ-wp2qHC zwkj{19oE?jl>Y_9iS2v-7+o-Z%dC(e(8^gRrKQoIq~PRz17iaw5n(;ufk* zxv#)g8INvX8KvFU113oud;Q|IPv$8+Ln!$K(lB-!-! zb4IfBR4tC2KY;l?!Cb0tm}hG#4-Z^~WooBIE#JCxzErmJrNwqRV@S@^?`#Dk@lR`3 zlX{i&v#+qS`gZ;>=@tKP|OV*y9{W>#i&A+~MckdeAdrVeydf8mbx#Z7~ zS8Z;_sk+2_rk0)ju0dTA07at)a43<-2KGD5gXLc=0tm9GIk~lXsn7?=>z@^t8ggL) zQqxZqfisi(S&QZ6M4+>+#kg2i4&2vR%N(u(?2Y1#YdBH?L;%yd)>mh6d{ZWa+Ypm=&tCy~+3)vaA2?h1%_D#*k zMvA_W7%8!a6wX(uRlo3&n`@7p*<;^Z%#Pg4HsV9ZGf3cD>Yd9byKDItrfEQMk@<9yjVUnT!fXIsr$r+u6J|j zWo>C)XPNGnDC{%sbtjditNzz>Hw7NE9&T)tc@bI;En=9P~!WkGnIrsV2Ymvc1qLEau=G)RK#eR*oJ67@NyLtdR|l9)6Z-Y}F1 z!hf+ny5{P&7-;0SrBD5u_1i37yQ%S2NZ{+YXIpVu>=&We5tVRh z-Fj5$tm!rKkt{Z?3c2+j~1EV;$O?^ z?pb^N{IMkTf;&ZxO^kp<-(qPj@otUN{H-bf5A;Qr0%SQbq#k94I;A60{q`AMlVE@RTh*AHJfalY?k>RO#JB=Dpzy%%~wE4U5AiQ zCh7qO#?cY#9VR&nk=AvP9MoFGCi=gJ}fJ zb<=jXoh4H*mhIhQq`fM)Go`Taib>uFF*p|S93L483(KMT4st1^tcf7u>jw38f(VrF z4wj8?B~HQKSRigdHcl@s8xy2E=|LtcRB~~?u{v%_Y_AtbU+gUXAygMu%OyLK76!nq z4ZubVZ!A$Rpn9j+g$m+jL8)e|{cm`Hj8vG^Z{i|CdqAFQuYpY?~b7b@|u< zuwU)a*pXX^p7D%0vmF^6)at+gub2)5 zf9V_z>H*}SxY!_?I=1yyh-j4K+C<89Nd><%T>9kZOP%ihTTPj(@@9P{Zxqf@)~eRT z5cWC+A_q+TP;33eyRfrkZydd6n_+11W~z}gu9lr|86~3{qBvAh>4Gi8_~++p*-Oq} z%qD`4SU6G4R%Sosi4=G8m0U72bA)!0!V`TAguzzpl|@oQh)Jnc8LuXQ$ruK`oTXKC zicfbIg72WIp_uo~#K9l!a=-v{Mhoe`O!GT9GNEh(+j=$0%Ng~zWLMst9Ig<>UEZ&P z5(PI|mY!(7-_sC6pwM+y!xq2-t9CR(J&&t2D>g5U_z1J>ZI1+9h^ z)rKb4b6#q z2}xOH2-P-|GugCb*+w|4LTl@iRUlhqpg=Gz^=84c5)`dGBa>i&!*tGl5BP+3qN zEA^NP%pX7l8E_dr-K_&80~}R#jiOh{kMK)GS^Z;QLT?;M0_uL#N;HY4$Lfo zA=~hx+Nb3WMl@kE(Z1LX>h;NTel|H}1L~FI`##OeX9Y47owGfJ7@4kR7P89|VJ;TL`NT zO&?!cqp^N^3wm+K*BTk-WSZz|EJp-=URAwi52?0o`{$1#NL_(UQQh~y5bR(PK<;Hh z(qGVTf!`>3V6r`^d(qS`M})*1(pvyh1ZOZf}`5RM@+on1A(5KWp(p7Q)+|nFaOyxY?efF%Ow* zz{5p>d~(9ol%25FySi!R9so=MEGYW*-1zmWqVf(OJ7MdJi8yca1R)ENGefqe4N7+ z;~Gb9e*mXxbA?Ff>#D)Su#Wv%SnaZ ztySh=0C0r#*SPH(fBQ4Q=)_mmAq<566%+{)H5zxoAeS9Y7%c5yUTNHSufR-Eq9r*rZP|%(!QjjUw*2 zT(dxdhHC@x%;&f{H*C_sdX%W; zbK-{PDTapsq*|_WiqdoB0QY!6iDb5Hh~lAe-E;s50DffMA&!aU**l^68n+r=I>!Bv zrJ>UDmeY*szKPDpW0r}+i1_|>@3hM22J-H*18ncmg50898HCAY( zReRz~YJO?tQiXZ6HN&2Uea;AZ0-UBv7xAR5wH3MUU=>UtzU}}fE=INqN$#~4z}-^I z1_~nLCWM6L(R^Dq6fSWHnS3t680S&{OLk+#knJm8fL&Z404B1hL{HAX2aJ{8n znDj#CyMAj(A~%94_>e98&amE9Ra2*DL$G|HLr&dty>S|i#awDh%$&N$y?CY&ITC#) z)vKQ~*?NBsNLdqH`N9FrjBX@5H&3*X@G&7o`GXV1Yf4@B>HXx#mn@Y2=rW#>7dfe8 z>MBl1^ro9}9qQT44h=2w(@|)!qoMz_Sc=3YHCWW z56RrjruLU%@w@U{4*fHrSG-2JYtNclL@sRHoMmSJaR^|f6A$$uSDSt_JL$26`$oj IdGY@L0#J<8CIA2c literal 0 HcmV?d00001 diff --git a/tests/visual/fixtures/elbow-arrows.excalidraw b/tests/visual/fixtures/elbow-arrows.excalidraw new file mode 100644 index 0000000..697ca9c --- /dev/null +++ b/tests/visual/fixtures/elbow-arrows.excalidraw @@ -0,0 +1,93 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "visual-test", + "elements": [ + { + "id": "elbow-L", + "type": "arrow", + "x": 50, + "y": 50, + "width": 200, + "height": 150, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "seed": 9001, + "angle": 0, + "elbowed": true, + "points": [[0, 0], [200, 0], [200, 150]], + "startArrowhead": null, + "endArrowhead": "arrow", + "isDeleted": false + }, + { + "id": "elbow-Z", + "type": "arrow", + "x": 50, + "y": 280, + "width": 300, + "height": 100, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "seed": 9002, + "angle": 0, + "elbowed": true, + "points": [[0, 0], [100, 0], [100, 100], [300, 100]], + "startArrowhead": "arrow", + "endArrowhead": "arrow", + "isDeleted": false + }, + { + "id": "elbow-U", + "type": "arrow", + "x": 50, + "y": 450, + "width": 200, + "height": 120, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 4, + "roughness": 1, + "opacity": 100, + "seed": 9003, + "angle": 0, + "elbowed": true, + "points": [[0, 0], [0, 120], [200, 120], [200, 0]], + "startArrowhead": null, + "endArrowhead": "arrow", + "isDeleted": false + }, + { + "id": "regular-curved", + "type": "arrow", + "x": 350, + "y": 50, + "width": 200, + "height": 150, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 2, + "roughness": 1, + "opacity": 100, + "seed": 9004, + "angle": 0, + "points": [[0, 0], [100, 75], [200, 150]], + "startArrowhead": null, + "endArrowhead": "arrow", + "isDeleted": false + } + ], + "appState": { + "viewBackgroundColor": "#ffffff" + } +}