0

I have a simple custom TikZ path style, which may be reduced to the following style:

\tikzset{custom/.style={
    to path={
        (\tikztostart) ..
        controls ($(\tikztostart)!.3!(\tikztotarget)$)
            and ($(\tikztostart)!.7!(\tikztotarget)$)
        .. (\tikztotarget)\tikztonodes}
}}

(In this example the control points are essentially useless, because there is no curve, but this is representative of the problem with my actual style, which is more complicated.)

For paths between nodes with short labels, this is fine. However, for nodes whose labels are longer than 30% of the distance between the centre of the start node and the centre of the target node, there are rendering issues, because the control points lie within the node label, and so arrowheads point in the wrong direction.

For example, the code:

\documentclass{article}

\usepackage{tikz-cd}
\usetikzlibrary{calc}

\begin{document}

\tikzset{custom/.style={
    to path={
        (\tikztostart) ..
        controls ($(\tikztostart)!.3!(\tikztotarget)$)
            and ($(\tikztostart)!.7!(\tikztotarget)$)
        .. (\tikztotarget)\tikztonodes}
}}

\[\begin{tikzcd}
    X & X
    \arrow[custom, from=1-1, to=1-2]
\end{tikzcd}\]

\[\begin{tikzcd}
    XXXXXXXXX & XXXXXXXXX
    \arrow[custom, from=1-1, to=1-2]
\end{tikzcd}\]

\end{document}

produces:

Incorrect arrowhead direction for nodes with long labels

Notice that the first arrow is fine, but the second arrow has the arrowhead pointing in the wrong direction.

My mental model for the path calculation here is obviously incorrect. I expect \tikztostart to refer to the centre of the start node, and \tikztoend to refer to the centre of the end node. (Certainly this seems to be the case if we make the control points vertically offset, introducing a curve.) Then (\tikztostart)!.3!(\tikztotarget) represents a point 30% of the distance from the centre of the start node to the centre of the end node, and similarly for (\tikztostart)!.7!(\tikztotarget). The path should then be trimmed so that it lies outside the bounding box of the start and end nodes, and an arrowhead drawn at the end of the path.

However, this is clearly not what is happening, otherwise the arrowhead would point in the correct direction: instead, the path appears to be starting at the point where the path is first visible (i.e. to the right of the start node bounding box), and ending at the point where the path is last visible (i.e. to the left of the target node bounding box). The control points do then appear to be based on the centre of the start and end nodes, as I expect. But I don't understand this behaviour, because \tikztostart and \tikztoend appears to be behaving differently for (1) the start and end co-ordinates and (2) the control points. I.e. for (1) they are based on the path after cropping to not overlap the label bounding boxes, whereas for (2) they are based on the node centres.

I therefore have two questions:

  1. What is wrong with my mental model, as described above?
  2. How can the path style be fixed so that it behaves as I expect it to (i.e. so that it fits my mental model above)? (In particular, this would mean that the arrowhead always points in the correct direction.)
7
  • The path starts at the border of the start node, goes towards the center of the start node and makes a 180° turn. The same happens at the target node but in reverse. This is why the arrow is that way. It points away from the control point inside of the node. If you want the calculations to only consider the border you will need to find a point on the border that you throw in the calculations. This can be done on the PGF level, manually or with an extra invisible path (TikZ-CD does something similar for shift left and various other transformation keys). Commented Nov 7, 2023 at 22:52
  • @Qrrbrbirlbel: thanks, I figured this was what was happening, but don't understand why. I would have expected the point on the border of the start node to be calculated after calculating the path, because if a large upwards curve is introduced into the path, then the point on the border is correctly placed on the top of the start node, rather than on the right of the start node. So I think I am confused about the order in which TikZ is rendering the path.
    – varkor
    Commented Nov 7, 2023 at 23:01
  • 1
    Inside a $ calculation, the specification (<node name>) will always refer to the center. In (<node name>) .. (<coordinate>) TikZ finds the point on the border of the node that is in direction of (<coordinate>), no matter whether that coordinate is outside of the node. I don't understand what you mean by the calculations or their order. Tikz looks at the start node and the 1st control point and puts the start point on the border do the node. Then it looks at the target node and the 2md control point and finds the border. These four points make up the Bézier curve. Commented Nov 7, 2023 at 23:52
  • 1
    It doesn't calculate the curve from the center of the nodes and then removes the part that's inside the node. (Libraries intersections and spath3 can do it, kinda, but it's not the most convenient approach, though with a limited set of nodes we can make it as good as possible.) As I said, we could manually find the border points in the direction of the control points and use them for the .3/.7 calculation. The curve will be a bit different, of course, then, but it's much less work. (How important is it how the curves will be calculated?) Commented Nov 7, 2023 at 23:57
  • @Qrrbrbirlbel: thanks, that resolves my confusion. I hadn't realised that (<node name>) behaved differently in those two different contexts (this is counterintuitive to me). As you say, in that case no cropping is necessary. I need to think a little bit about what an acceptable solution would be in light of this. I'll reply again in a little while.
    – varkor
    Commented Nov 8, 2023 at 0:00

1 Answer 1

2

Code

\documentclass[varwidth]{standalone}
%\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{cd, intersections, spath3, bending}% always bending with curves
\usetikzlibrary{decorations.pathreplacing, ext.arrows}% only for this document
\tikzset{
  debug curve/.style={
    postaction=decorate,
    decoration={name=show path construction, curveto code={
      \path[thin,-{Centered Circle[open]},line to]
      (\tikzinputsegmentfirst) edge (\tikzinputsegmentsupporta)
      (\tikzinputsegmentlast ) edge (\tikzinputsegmentsupportb);}}}}
\begin{document}
In the following we will assume that \verb|\tikztostart| and \verb|\tikztotarget|
will hold names of rectangular nodes.
This usually means the shape \verb|rectangle| but in the case of TikZ-CD,
the shape \verb|asymmetrical rectangle| is actually used.
It will not change our approach, though.

This means, all this will fail if a normal coordinate like \verb|(0, 0)| or
an anchor specification like \verb|(Foo.north east)| is used.

In a TikZ-CD where the nodes usually have no drawn border and
the \verb|outer sep|s should have been set to zero, there's almost no difference
in whether we find a point \emph{on} the line of the node's path or
\emph{on the border} of a node but in the examples below
I will use exaggerated node and path styles.

\tikzset{
  exaggerated node style/.style={% this is “shape example”
    color=black!30, draw, fill=yellow!30,
    line width=+.15cm, inner xsep=+.7cm, inner ysep=+.5cm},
  exaggerated path style/.style={black, line width=+.1cm}}
\bigskip

We will use a \verb|to path| that is provided
by the always loaded \verb|topaths| library.
Let's draw an \verb|s bend|.
\makeatletter
\tikzset{
  s bend/.style={relative, out={-(#1)}, in={180-(#1)}, distance=3cm},
  s bend/.default=\tikz@to@bend,
}% support for “bend angle”
\makeatother
\tikzcdset{
  exaggerated/.style={
    cells={nodes=exaggerated node style},
    arrows={exaggerated path style, debug curve},
    column sep=1cm, math mode=false}}

Let's look at our curve between two nodes in a TikZ-Cd environment:
\[
\begin{tikzcd}[exaggerated]
Start \drar[s bend, gray, "Whee!"  sloped, start anchor=center, end anchor=center]
      \drar[s bend,       "Whoah!" sloped] & \\ & End
\end{tikzcd}
\]

We want the gray curve but not the part inside the node.
The black one looks totally different!
\bigskip

Let's find the intersection between the curve and the nodes
and use \verb|spath3| to trim the curve to.
For this, we will have every node save its path.
\textbf{However, this won't work with a matrix}
which is also used internally by TikZ-CD.
\makeatletter
\tikzset{
  save my path/.style={spath/save global=s bend \tikz@fig@name},
  s bend spath/.default=\tikz@to@bend,
  s bend spath/.style={
    to path={
      \pgfextra
      \pgfinterruptpath
        \expanded{% no clear tonodes?
          \noexpand\path (\tikztostart.center) to[s bend={#1}]
                         (\tikztotarget.center) [spath/save global=s bend curve];}%
      \endpgfinterruptpath
      \endpgfextra
      [spath/.cd,
        split at intersections with/.expanded={s bend curve}{s bend \tikztostart },
        split at intersections with/.expanded={s bend curve}{s bend \tikztotarget},
        remove components={s bend curve}{1, 3},
        use=s bend curve,
      ] \tikztonodes
    }
  }
}
\makeatother

Note, how the black line starts at the middle of the path of the nodes.
I don't like it, this totally ignores any \verb|outer sep| settings.
(Again, for nodes whose border aren't stroked or are very thin
this is barely noticeable.)
\[
\begin{tikzpicture}[sloped]
\path[nodes={exaggerated node style, save my path}]
  node (Start)            {Start}
  node (End)   at (3, -2) {End};
\path[-tikzcd to, exaggerated path style, every edge/.append style=debug curve]
  (Start.center) edge[s bend, gray]                     (End.center)
  (Start)        edge[s bend spath] node[above] {Whee!} (End);
\end{tikzpicture}
\]
But we can't use this with matrices anyway \dots\bigskip

However, as we only use rectangular nodes, we can reconstruct the path easily.
Once again, I will assume that the nodes are not transformed
and that there's no transformation set when we draw the curve.

\makeatletter
\tikzset{
  clear tonodes/.style={execute at begin to=\let\tikztonodes\pgfutil@empty},
  s bend spath matrix/.default=\tikz@to@bend,
  s bend spath matrix/.style={
    to path={
      \pgfextra
      \pgfinterruptpath
        \path (\tikztostart.south west) rectangle (\tikztostart.north east)
              (\tikztotarget.south west) rectangle (\tikztotarget.north east)
              [spath/save global=s bend rect];
        \expanded{%
          \noexpand\path[clear tonodes] (\tikztostart.center) to[s bend={#1}]
            (\tikztotarget.center) [spath/save global=s bend curve];}%
      \endpgfinterruptpath
      \endpgfextra
      [spath/.cd,
        split at intersections with/.expanded={s bend curve}{s bend rect},
                  remove components={s bend curve}{1, 3},
                  use=s bend curve
      ] \tikztonodes
    }
  }
}
\makeatother

\[
\begin{tikzcd}[exaggerated]
Start \drar[s bend, gray, start anchor=center, end anchor=center]
      \drar[s bend spath matrix, "Whee!" sloped] & \\ & End
\end{tikzcd}
\]
\end{document}

Output

enter image description here

enter image description here

3
  • This post isn't really accessible, is it? :( — We don't actually need spath3 if we just want to trim one Bézier curve since we can extract the times on the curve of the intersections and then use \pgfpathcurvebetweentime but spath3 does this for us, provides a timer to place nodes on the new curve and we can apply this approach to any paths that might have been constructed out of many segments. To cover all possible combination and shapes and transformation, more work is necessary. Commented Nov 8, 2023 at 11:14
  • This looks like an excellent answer, thank you! It will take me a little time to digest, and I don't have time until tomorrow or Friday, but will respond again when I have had a chance to do so!
    – varkor
    Commented Nov 8, 2023 at 21:24
  • Sorry, I have been very busy since I posted this, and I haven't had time to read your answer in detail yet. I will do so as soon as I have time.
    – varkor
    Commented Nov 25, 2023 at 0:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .