3

Continuing http://tex.stackexchange.com/a/111674 and TIKZ - automatic bridge if two paths cross, we would like to draw Bézier curves in such a way that whenever they cross, one curve forms a bridge above the other curve, and the lower curve fades out (from black to white) when approaching the bridge and fades in from white to black right after exiting the bridge). Has anyone, perhaps, already done the job? Here are some bits of it, mostly stolen from TeX.SE and Github:

\documentclass{article}
\pagestyle{empty}
\usepackage{tikz}
\usetikzlibrary{calc,positioning,bbox,intersections,angles,fadings}%%% get pgflibrarybbox.code.tex from https://github.com/pgf-tikz/pgf/issues/856#issuecomment-632925356 so as to show arrow tips
\newcommand{\bridgeRadius}{.5ex}
\begin{document}
\noindent Bridge:
\begin{center}%
  \begin{tikzpicture}[atomicNode/.style={rectangle,draw,minimum width=5em, minimum height=3ex}]
    \node[atomicNode] (SOne) {\(S_1\)};
    \coordinate (SOneZOneOut) at ($(SOne.north west)!.8!(SOne.north east)$);
    \coordinate (SOneZTwoIn) at ($(SOne.south west)!.8!(SOne.south east)$);
    \node[atomicNode,right=10em of SOne] (STwo) {\(S_2\)};
    \coordinate (STwoZTwoOut) at ($(STwo.north west)!.2!(STwo.north east)$);
    \coordinate (STwoZOneIn) at ($(STwo.south west)!.2!(STwo.south east)$);
    \begin{scope}[bezier bounding box=true]
      \path[name path=pathBezierZTwo,-latex] (SOneZTwoIn) .. controls ($(SOneZTwoIn)-(0,3ex)$) and ($(STwoZTwoOut)+(0,3ex)$) .. (STwoZTwoOut);
      \path[name path=pathBezierZOne,-latex] (STwoZOneIn) .. controls ($(STwoZOneIn)-(0,3ex)$) and ($(SOneZOneOut)+(0,3ex)$) .. (SOneZOneOut);
      \path[name intersections={of=pathBezierZTwo and pathBezierZOne,by=inter}];
      \path let \p1=(SOneZTwoIn), \p2=($(SOneZTwoIn)-(0,3ex)$), \p{dir}=($(\p2)-(inter)$), \n{len}={\bridgeRadius/veclen(\p{dir})}, \p{scaleddir}=($\n{len}*(\p{dir})$) in coordinate (startHalfCircleOnZTwo) at ($(inter)+(\p{scaleddir})$);
      \path let \p1=(STwoZTwoOut), \p2=($(STwoZTwoOut)+(0,3ex)$), \p{dir}=($(\p2)-(inter)$), \n{len}={\bridgeRadius/veclen(\p{dir})}, \p{scaleddir}=($\n{len}*(\p{dir})$) in coordinate (endHalfCircleOnZTwo) at ($(inter)+(\p{scaleddir})$);
      \path let \p1=(STwoZOneIn), \p2=($(STwoZOneIn)-(0,3ex)$), \p{dir}=($(\p2)-(inter)$), \n{len}={\bridgeRadius/veclen(\p{dir})}, \p{scaleddir}=($\n{len}*(\p{dir})$) in coordinate (startHalfCircleOnZOne) at ($(inter)+(\p{scaleddir})$);
      \path let \p1=(SOneZOneOut), \p2=($(SOneZOneOut)+(0,3ex)$), \p{dir}=($(\p2)-(inter)$), \n{len}={\bridgeRadius/veclen(\p{dir})}, \p{scaleddir}=($\n{len}*(\p{dir})$) in coordinate (endHalfCircleOnZOne) at ($(inter)+(\p{scaleddir})$);
      \draw[-latex] (STwoZOneIn) .. controls ($(STwoZOneIn)-(0,3ex)$) .. (startHalfCircleOnZOne) -- (endHalfCircleOnZOne) .. controls ($(SOneZOneOut)+(0,3ex)$) .. node[above,inner sep=.22ex,pos=.59](ZOneNorthText){\(z_1\)} (SOneZOneOut);
      \draw (SOneZTwoIn) .. controls ($(SOneZTwoIn)-(0,3ex)$) .. (startHalfCircleOnZTwo);
      \pic [draw, angle radius=\bridgeRadius] {angle=endHalfCircleOnZTwo--inter--startHalfCircleOnZTwo};
      \draw[-latex] (endHalfCircleOnZTwo) .. controls ($(STwoZTwoOut)+(0,3ex)$) ..  node[above,inner sep=.22ex,pos=.59](ZOneNorthText){\(z_2\)} (STwoZTwoOut);
    \end{scope}
    \draw (current bounding box.north west) rectangle (current bounding box.south east);
  \end{tikzpicture}%
\end{center}
Fading:
\begin{center}%
  \makeatletter
  \newif\iftikz@shading@path
  \tikzset{
    % There are three circumstances in which the fading sep is needed:
    % 1. Arrows which do not update the bounding box (which is most of them).
    % 2. Line caps/joins and mitres that extend outside the natural bounding 
    % box of the path (these are not calculated by PGF).
    % 3. Other reasons that haven't been anticipated.
    shading xsep/.store in=\tikz@pathshadingxsep,
    shading ysep/.store in=\tikz@pathshadingysep,
    shading sep/.style={shading xsep=#1, shading ysep=#1},
    shading sep=0.0cm,
  }
  \def\tikz@shadepath#1{% 
    % \tikz@addmode installs the `modes' (e.g., fill, draw, shade) 
    % to be applied to the path. It isn't usualy for doing more
    % changes to the path's construction.
    \iftikz@shading@path%
    \else%
    \tikz@shading@pathtrue%
    % Get the current path.
    \pgfgetpath\tikz@currentshadingpath%
    % Get the shading sep without setting any other keys.
    \begingroup%
    \pgfsys@beginscope% <- may not be necessary
    \tikzset{#1}%
    \xdef\tikz@tmp{\noexpand\def\noexpand\tikz@pathshadingxsep{\tikz@pathshadingxsep}%
      \noexpand\def\noexpand\tikz@pathshadingysep{\tikz@pathshadingysep}}%
    \pgfsys@endscope%
    \endgroup
    \tikz@tmp%
    % Get the boudning box of the current path size including the shading sep
    \pgfextract@process\pgf@shadingpath@southwest{\pgfpointadd{\pgfqpoint{\pgf@pathminx}{\pgf@pathminy}}%
      {\pgfpoint{-\tikz@pathshadingxsep}{-\tikz@pathshadingysep}}}%%
    \pgfextract@process\pgf@shadingpath@northeast{\pgfpointadd{\pgfqpoint{\pgf@pathmaxx}{\pgf@pathmaxy}}%
      {\pgfpoint{\tikz@pathshadingxsep}{\tikz@pathshadingysep}}}%
    % Clear the path
    \pgfsetpath\pgfutil@empty%                          
    % Save the current drawing mode and options.
    \let\tikz@options@saved=\tikz@options%
    \let\tikz@mode@saved=\tikz@mode%
    \let\tikz@options=\pgfutil@empty%
    \let\tikz@mode=\pgfutil@empty%
    % \tikz@options are processed later on.
    \tikz@addoption{%
      \pgfinterruptpath%
      \pgfinterruptpicture%
      \begin{tikzfadingfrompicture}[name=.]
        \pgfscope%
        \tikzset{shade path/.style=}% Make absolutely sure shade path is not inherited.
        \path \pgfextra{%
          % Set the softpath. Any transformations,draw=none} in #1 will have no effect.
          % This will *not* update the bounding box...
          \pgfsetpath\tikz@currentshadingpath%
          % ...so it is done manually.
          \pgf@shadingpath@southwest
          \expandafter\pgf@protocolsizes{\the\pgf@x}{\the\pgf@y}%
          \pgf@shadingpath@northeast%
          \expandafter\pgf@protocolsizes{\the\pgf@x}{\the\pgf@y}%
          % Install the drawing modes and options.
          \let\tikz@options=\tikz@options@saved%
          \let\tikz@mode=\tikz@mode@saved%
        };
        % Now get the bounding box of the picture.
        \xdef\pgf@shadingboundingbox@southwest{\noexpand\pgfqpoint{\the\pgf@picminx}{\the\pgf@picminy}}%
        \xdef\pgf@shadingboundingbox@northeast{\noexpand\pgfqpoint{\the\pgf@picmaxx}{\the\pgf@picmaxy}}%
        \endpgfscope
      \end{tikzfadingfrompicture}%
      \endpgfinterruptpicture%
      \endpgfinterruptpath%
      % Install a rectangle that covers the shaded/faded path picture.
      \pgftransformreset%
      \pgfpathrectanglecorners{\pgf@shadingboundingbox@southwest}{\pgf@shadingboundingbox@northeast}%
      % 
      % Reset all modes.
      \let\tikz@path@picture=\pgfutil@empty%
      \tikz@mode@fillfalse%
      \tikz@mode@drawfalse%
      % \tikz@mode@tipsfalse%   <- To have successful compilation with pgf-tikz v3.0.1a
      \tikz@mode@doublefalse%
      \tikz@mode@clipfalse%
      \tikz@mode@boundaryfalse%
      \tikz@mode@fade@pathfalse%
      \tikz@mode@fade@scopefalse%
      % Now install shading options.
      \tikzset{#1}%
      \tikz@mode%
      % Make the fading happen.
      \def\tikz@path@fading{.}%
      \tikz@mode@fade@pathtrue%
      \tikz@fade@adjustfalse%
      % Shift the fading to the mid point of the rectangle
      \pgfpointscale{0.5}{\pgfpointadd{\pgf@shadingboundingbox@southwest}{\pgf@shadingboundingbox@northeast}}%
      \edef\tikz@fade@transform{shift={(\the\pgf@x,\the\pgf@y)}}%
      \pgfsetfading{\tikz@path@fading}{\tikz@do@fade@transform}%
      \tikz@mode@fade@pathfalse%              
    }%
    \fi%
  }
  \tikzset{
    shade path/.code={%
      \tikz@addmode{\tikz@shadepath{#1}}%
    }
  }
  \makeatother % <- To close the \makeatletter call
  \begin{tikzpicture}[atomicNode/.style={rectangle,draw,minimum width=2em,minimum height=2ex}]
    \coordinate (center);
    \node[atomicNode,left=of center] (A) {};
    \node[atomicNode,right=of center] (B) {};
    \node[atomicNode,above=of center] (C) {};
    \node[atomicNode,below=of center] (D) {};
    \coordinate (Aout) at (A.east);
    \coordinate (Bin) at (B.west);
    \coordinate (Cout) at (C.south);
    \coordinate (Din) at (D.north);
    \draw[-] (Aout) -- ($(center)-(.4999ex,0)$);
    \draw[draw=transparent!0,-,shade path={shading xsep=.01ex, left color=black, right color=white}] ($(center)-(.5001ex,0)$) -- ($(center)-(.2pt,0)$);
    \path[draw=transparent!0,-,shade path={shading xsep=.01ex, left color=white, right color=black}] ($(center)+(.2pt,0)$) -- ($(center)+(.5001ex,0)$);
    \draw[-latex] ($(center)+(.4999ex,0)$) -- (Bin);
    \draw[-latex] (Cout) -- (Din);
  \end{tikzpicture}
\end{center}
\end{document}

Running pdflatex on it gives this:

compilation result

Here is the screenshot of the crossing under 415,8% magnification of the PDF viewed in evince:

magnified crossing

The Bézier curve bridge suffers from various problems itself:

  • The intersection of Bézier curves is computed on the basis of one pair of curves, where as in reality a slightly different pair of curves is drawn. We are simply extremely lucky to have these two curves very symmetrical: I wouldn't know how to draw another pair of curves with a bridge in general.

  • The "lower" z₁ Bézier curve is drawn with a straight line segment in the middle (so that it is drawn symmetrically to the "upper" z₂ Bézier curve), whereas in general the lower curve should be "curved". In our case, the difference is unnoticeable, but in general, it can be very well noticeable.

For the fading drawing, the "lower" line does not only fade out and then fade in, but it also gets thinner. However, the thickness of the fading part should remain the same.

I don't know how to solve these issues separately, let alone together. Independent solutions for these issues are welcome.

Moreover, if anyone would have a macro such as, say, \drawBezierCurvesWithBridgesFadingAndAnnotations{curvename1}{labelnodespecification1}{label1}{curvename2}{labelnodespecification2}{label2}, which gets named paths, the label placement options, and the labels, and draws all this with a bridge at the crossing and makes the lower curve fade when disappearing below the bridge, please feel free... (Btw, I'd like to have bridges ∩ rather than tunnels ∪ !)

2 Answers 2

5

The bridge is not too difficult to construct:

  1. Draw a curve A and construct the path of the other curve, B, while saving it.
  2. Compute the intersection, inter, of A and B.
  3. Construct a circle around inter.
  4. Use a reversed clip when drawing B so that it has a gap around inter.
  5. Compute the intersections of the circle and B.
  6. Use these intersections to draw the arc while making sure that it is a bridge.

enter image description here

Code:

\documentclass[tikz,border=3mm]{standalone}
\usetikzlibrary{intersections,angles,calc}
\begin{document}
\begin{tikzpicture}
 \newcommand{\bridgeRadius}{.5ex}
 \begin{scope}
  \draw[name path=A] (1,0) to[bend right] (0,2);
  \path[name path=B,save path=\pathB] (0,0) to[bend left] (2,2);
  \path[name intersections={of=A and B,by=inter}];
  \path[name path=C] (inter) circle[radius=\bridgeRadius];
  \path[name intersections={of=C and B,name=i}];
  \begin{scope}
   \clip (inter) circle[radius=\bridgeRadius] (current bounding box.south west) |-
    (current bounding box.north east) |- cycle;
   \draw[use path=\pathB,-latex]; 
  \end{scope}
  \path let \p1=($(i-1)-(i-2)$),\n1={scalar(int(sign(\x1)))} in
   \ifnum\n1=-1
    pic [draw,line cap=round, angle radius=\bridgeRadius] {angle=i-2--inter--i-1}
   \else 
    pic [draw,line cap=round, angle radius=\bridgeRadius] {angle=i-1--inter--i-2}
   \fi; 
 \end{scope} 
 \begin{scope}[xshift=3cm]
  \draw[name path=A] (1,0) to[bend right] (0,2);
  \path[name path=B,save path=\pathB] (2,2) to[bend left] (0,0);
  \path[name intersections={of=A and B,by=inter}];
  \path[name path=C] (inter) circle[radius=\bridgeRadius];
  \path[name intersections={of=C and B,name=i}];
  \begin{scope}
   \clip (inter) circle[radius=\bridgeRadius] (current bounding box.south west) |-
    (current bounding box.north east) |- cycle;
   \draw[use path=\pathB,-latex]; 
  \end{scope}
  \path let \p1=($(i-1)-(i-2)$),\n1={scalar(int(sign(\x1)))} in
   \ifnum\n1=-1
    pic [draw,line cap=round, angle radius=\bridgeRadius] {angle=i-2--inter--i-1}
   \else 
    pic [draw,line cap=round, angle radius=\bridgeRadius] {angle=i-1--inter--i-2}
   \fi; 
 \end{scope} 
\end{tikzpicture}
\end{document}

Here is a style doing the same. (I am not claiming that the usage us particularly intuitive but you can adjust it to your needs.) It does some sanity checks such as verifying that the paths really intersect.

\documentclass[tikz,border=3mm]{standalone}
\usetikzlibrary{intersections,angles,calc}
\makeatletter
\tikzset{bridge/.code args={#1 over #2}{%
 \path[name path=tmp@bridge@path@B] #1;
 \tikzset{name intersections={of=#2 and tmp@bridge@path@B,
    by=tmp@i@0,total=\tmp@i@total}}
 \ifnum\tmp@i@total=0
  \typeout{These paths do not intersect. No bridge, sorry.}%
 \else
  \path[name path=tmp@bridge@path@C] (tmp@i@0) 
    circle[radius=\pgfkeysvalueof{/tikz/bridge radius}]; 
  \tikzset{name intersections={of=tmp@bridge@path@B and tmp@bridge@path@C,
    by={tmp@i@1,tmp@i@2},total=\tmp@i@total}}
  \ifnum\tmp@i@total=2
   \begin{scope}
    \clip (tmp@i@0) circle[radius=\pgfkeysvalueof{/tikz/bridge radius}]
     (current bounding box.south west) |-
     (current bounding box.north east) |- cycle;
    \draw[bridge-style] #1; 
   \end{scope}  
   \path let \p1=($(tmp@i@1)-(tmp@i@2)$),\n1={scalar(int(sign(\x1)))} in
    \ifnum\n1=-1
     pic [draw,line cap=round, angle radius=\pgfkeysvalueof{/tikz/bridge radius},
        bridge-arc] {angle=tmp@i@2--tmp@i@0--tmp@i@1}
    \else 
     pic [draw,line cap=round, angle radius=\pgfkeysvalueof{/tikz/bridge radius},
        bridge-arc] {angle=tmp@i@1--tmp@i@0--tmp@i@2}
    \fi; 
  \else
   \typeout{Given the bridge radius \pgfkeysvalueof{/tikz/bridge radius}, 
    the path is not suited to construct a nice bridge. No bridge, sorry.}%
  \fi   
 \fi 
},bridge radius/.initial=.5ex,
    bridge style/.code={\tikzset{bridge-style/.style={#1}}},
    bridge arc/.code={\tikzset{bridge-arc/.style={#1}}},
    bridge style={},bridge arc={}}
\makeatother
\begin{document}
\begin{tikzpicture}
 \begin{scope}
  \draw[name path=A] (1,0) to[bend right] (0,2);
  \tikzset{bridge={(0,0) to[bend left] (2,2)} over A}
 \end{scope} 
 \begin{scope}[xshift=3cm]
  \draw[name path=A] (1,0) to[bend right] (0,2);
  \tikzset{bridge style={-latex},bridge={(2,2) to[bend left] (0,0)} over A}
 \end{scope} 
\end{tikzpicture}
\end{document}
5
  • Thx! Do you wish to say, perhaps, \clip (inter) circle[radius=\bridgeRadius]... instead of \clip (inter) circle[radius=0.5ex]... ? Further: your drawing has a bridge, not a tunnel; is there a way to automatically favor bridges (as opposed to tunnels) in general? Finally: how to construct an arrow at any end of the Bézier curve B? Feb 5, 2021 at 0:32
  • 1
    @GeekestGeek As for the 0.5ex: absolutely, good catch! As for the bridge: sure. only figure out whether or not (i-1) is left of (i-2), and switch the roles of the intersections, if necessary. And an arrow is as simple as e.g. \draw[use path=\pathB,-latex];. I added these, together with a test of whether the bridge thing really works. Notice also that this is just to devise some strategy, if you want to do bridges over and over, store the repeating steps in a style. This may define a new question, though, since it is not excluded that you get more information on the fading stuff.
    – user234180
    Feb 5, 2021 at 0:52
  • BTW, I decided to delete my account here. This site is incredibly sad in that those who steal from others get upvoted like crazy. This is not for me.
    – user234180
    Feb 5, 2021 at 17:12
  • I did steal some code from others (as stated in the OP), but only for time reasons. No harm meant. I will pay attention to citing the source better in the future. I apologize if I hurt folks here (potentially, including you). Feb 16, 2021 at 22:20
  • Sometimes there is no space in a drawing for a bridge, i.e., for „X over Y“. How to have a tunnel, i.e., „X under Y“, where the arc is drawn below rather than above the road crossed? Mar 18, 2021 at 15:34
2

I don't know about path fading, but here's a proof of concept for the bridges which is relatively automatic. It uses the newest (as of Feb 2021) version of the spath3 package which is now on the TeXLive servers.

Here's the code:

\documentclass{article}
%\url{https://tex.stackexchange.com/q/581540/86}

\usepackage{tikz}
\usetikzlibrary{spath3,intersections}

\begin{document}
\begin{tikzpicture}
\path[spath/save=over] (0,0) to[out=60,in=150] (3,3);
\path[spath/save=under] (0,3) to[out=30,in=120] (3,0);
\path[spath/save=arc] (0,0) arc[radius=1cm, start angle=180, delta angle=-180];
\tikzset{
  spath/split at intersections with={over}{under},
  spath/insert gaps after components={over}{8pt}{1},
  spath/join components with={over}{arc},
  spath/split at intersections with={under}{over},
  spath/insert gaps after components={under}{4pt}{1},
}
\draw[spath/restore=over];
\draw[spath/restore=under];
\end{tikzpicture}
\end{document}

Here's the result:

Inserting bridges at intersection points

What's happening here is the following:

  1. Three paths are defined: the two main paths and an arc (that will be the bridge)
  2. The intersection point is found from the point of view of the over path and the over path is split at that point.
  3. The over path is shortened (along its path) either side of that split point.
  4. The arc path is spliced in to the over path to fill in this gap (and the path is joined together to make a single continuous path).
  5. Now the under path is split where it intersects with this (new) over path, and a smaller gap is inserted at this split point. (If I knew about path fading, this could presumably used to fade the path at this point rather than splitting and inserting a gap.)
  6. Once all this is done, the paths are rendered.

Although there's only one intersection point in this drawing, the same code would work with multiple intersection points. What would need a bit of extra would be to ensure that all the arcs are "bridges" rather than "tunnels".

2
  • Thx! Could I kindly ask you to add arrow tips to the two Bézier curves, one arrow tip per curve? Feb 16, 2021 at 22:18
  • @GeekestGeek You should be able to do that just by adding -> to the draw commands. Feb 16, 2021 at 22:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.