4

I'm drawing a knot diagram and I use the \clip command to help shading the regions. However, it seems that the \strand command of the knot environment have some strange effect to \clip.

For instance, the correct result should be (let's call it Diagram 1):The desired result which is generated by

% Diagram 1
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{knots}
\usetikzlibrary{hobby}
\begin{document}
    \definecolor{skyblue}{RGB}{60,120,234}
    \scalebox{0.3}{\begin{tikzpicture}[use Hobby shortcut]
        \pgfdeclarelayer{foreground}
        \pgfsetlayers{main,foreground}
        \begin{pgfonlayer}{foreground}
            \begin{scope}
                \clip (-2,3) .. (0,2) .. (0.4,1)
                            .. (0,0) .. (-0.4,-1)
                            .. (0,-2) .. (2,-3)
                            .. (4.8,0) .. (2,3)
                            .. (0,2) .. (-0.4,1) -- (-2,3);
                \clip (-2,-3) .. (0,-2) .. (0.4,-1)
                            .. (0,0) .. (-0.4,1)
                            .. (0,2) .. (2,3)
                            .. (4.8,0) .. (2,-3)
                            .. (0,-2) .. (-0.4,-1) -- (-2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}
            \begin{scope}
                \clip (2,3) .. (0,2) .. (-0.4,1)
                            .. (0,0) .. (0.4,-1)
                            .. (0,-2) .. (-2,-3)
                            .. (-4.8,0) .. (-2,3)
                            .. (0,2) .. (0.4,1) -- (2,3);
                \clip (2,-3) .. (0,-2) .. (-0.4,-1)
                            .. (0,0) .. (0.4,1)
                            .. (0,2) .. (-2,3)
                            .. (-4.8,0) .. (-2,-3)
                            .. (0,-2) .. (0.4,-1) -- (2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}    
            \draw (-2.2,0) node[scale=3] {$+$};
            \draw (2.2,0) node[scale=3] {$-$};
            \draw (5,2.5) node[scale=3] {$M$};
        \end{pgfonlayer}
        \begin{knot}[
                        consider self intersections,
                        clip width=10,
                        clip radius=0.5cm,
                        ignore endpoint intersections=false,
                        flip crossing/.list={6,14}
                    ]
            \strand[very thick,black,closed] 
                    (0.4,1) .. (0,2) .. (-2,3) 
                            .. (-4.8,0) .. (-2,-3) 
                            .. (0,-2) .. (0.4,-1) 
                            .. (0,0) .. (-0.4,1) 
                            .. (0,2) .. (2,3) 
                            .. (4.8,0) .. (2,-3) 
                            .. (0,-2) .. (-0.4,-1) .. (0,0);
        \end{knot}
    \end{tikzpicture}}
\end{document}

Notice that \clip is before \strand.

However, if I switch the order of the code fragment containing \clip and \strand:

% Diagram 2
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{knots}
\usetikzlibrary{hobby}
\begin{document}
    \definecolor{skyblue}{RGB}{60,120,234}
    \scalebox{0.3}{\begin{tikzpicture}[use Hobby shortcut]
        \pgfdeclarelayer{foreground}
        \pgfsetlayers{main,foreground}
        \begin{knot}[
                        consider self intersections,
                        clip width=10,
                        clip radius=0.5cm,
                        ignore endpoint intersections=false,
                        flip crossing/.list={6,14}
                    ]
            \strand[very thick,black,closed] 
                    (0.4,1) .. (0,2) .. (-2,3) 
                            .. (-4.8,0) .. (-2,-3) 
                            .. (0,-2) .. (0.4,-1) 
                            .. (0,0) .. (-0.4,1) 
                            .. (0,2) .. (2,3) 
                            .. (4.8,0) .. (2,-3) 
                            .. (0,-2) .. (-0.4,-1) .. (0,0);
        \end{knot}
        \begin{pgfonlayer}{foreground}
            \begin{scope}
                \clip (-2,3) .. (0,2) .. (0.4,1)
                            .. (0,0) .. (-0.4,-1)
                            .. (0,-2) .. (2,-3)
                            .. (4.8,0) .. (2,3)
                            .. (0,2) .. (-0.4,1) -- (-2,3);
                \clip (-2,-3) .. (0,-2) .. (0.4,-1)
                            .. (0,0) .. (-0.4,1)
                            .. (0,2) .. (2,3)
                            .. (4.8,0) .. (2,-3)
                            .. (0,-2) .. (-0.4,-1) -- (-2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}
            \begin{scope}
                \clip (2,3) .. (0,2) .. (-0.4,1)
                            .. (0,0) .. (0.4,-1)
                            .. (0,-2) .. (-2,-3)
                            .. (-4.8,0) .. (-2,3)
                            .. (0,2) .. (0.4,1) -- (2,3);
                \clip (2,-3) .. (0,-2) .. (-0.4,-1)
                            .. (0,0) .. (0.4,1)
                            .. (0,2) .. (-2,3)
                            .. (-4.8,0) .. (-2,-3)
                            .. (0,-2) .. (0.4,-1) -- (2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}    
            \draw (-2.2,0) node[scale=3] {$+$};
            \draw (2.2,0) node[scale=3] {$-$};
            \draw (5,2.5) node[scale=3] {$M$};
        \end{pgfonlayer}
    \end{tikzpicture}}
\end{document}

The result becomes (let's call it Diagram 2): bad image As you can see, the shading doesn't fit into the curve.

Even if I use the code for generating Diagram 1 (where \clip is before \strand), the error would still appear when there are other \strand commands before the \clip code. One can see this by repeating the code for Diagram 1 twice:

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{knots}
\usetikzlibrary{hobby}
\begin{document}
    \definecolor{skyblue}{RGB}{60,120,234}
    \scalebox{0.3}{\begin{tikzpicture}[use Hobby shortcut]
        \pgfdeclarelayer{foreground}
        \pgfsetlayers{main,foreground}
        \begin{pgfonlayer}{foreground}
            \begin{scope}
                \clip (-2,3) .. (0,2) .. (0.4,1)
                            .. (0,0) .. (-0.4,-1)
                            .. (0,-2) .. (2,-3)
                            .. (4.8,0) .. (2,3)
                            .. (0,2) .. (-0.4,1) -- (-2,3);
                \clip (-2,-3) .. (0,-2) .. (0.4,-1)
                            .. (0,0) .. (-0.4,1)
                            .. (0,2) .. (2,3)
                            .. (4.8,0) .. (2,-3)
                            .. (0,-2) .. (-0.4,-1) -- (-2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}
            \begin{scope}
                \clip (2,3) .. (0,2) .. (-0.4,1)
                            .. (0,0) .. (0.4,-1)
                            .. (0,-2) .. (-2,-3)
                            .. (-4.8,0) .. (-2,3)
                            .. (0,2) .. (0.4,1) -- (2,3);
                \clip (2,-3) .. (0,-2) .. (-0.4,-1)
                            .. (0,0) .. (0.4,1)
                            .. (0,2) .. (-2,3)
                            .. (-4.8,0) .. (-2,-3)
                            .. (0,-2) .. (0.4,-1) -- (2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}    
            \draw (-2.2,0) node[scale=3] {$+$};
            \draw (2.2,0) node[scale=3] {$-$};
            \draw (5,2.5) node[scale=3] {$M$};
        \end{pgfonlayer}
        \begin{knot}[
                        consider self intersections,
                        clip width=10,
                        clip radius=0.5cm,
                        ignore endpoint intersections=false,
                        flip crossing/.list={6,14}
                    ]
            \strand[very thick,black,closed] 
                    (0.4,1) .. (0,2) .. (-2,3) 
                            .. (-4.8,0) .. (-2,-3) 
                            .. (0,-2) .. (0.4,-1) 
                            .. (0,0) .. (-0.4,1) 
                            .. (0,2) .. (2,3) 
                            .. (4.8,0) .. (2,-3) 
                            .. (0,-2) .. (-0.4,-1) .. (0,0);
        \end{knot}
    \end{tikzpicture}}

    \scalebox{0.3}{\begin{tikzpicture}[use Hobby shortcut]
        \pgfdeclarelayer{foreground}
        \pgfsetlayers{main,foreground}
        \begin{pgfonlayer}{foreground}
            \begin{scope}
                \clip (-2,3) .. (0,2) .. (0.4,1)
                            .. (0,0) .. (-0.4,-1)
                            .. (0,-2) .. (2,-3)
                            .. (4.8,0) .. (2,3)
                            .. (0,2) .. (-0.4,1) -- (-2,3);
                \clip (-2,-3) .. (0,-2) .. (0.4,-1)
                            .. (0,0) .. (-0.4,1)
                            .. (0,2) .. (2,3)
                            .. (4.8,0) .. (2,-3)
                            .. (0,-2) .. (-0.4,-1) -- (-2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}
            \begin{scope}
                \clip (2,3) .. (0,2) .. (-0.4,1)
                            .. (0,0) .. (0.4,-1)
                            .. (0,-2) .. (-2,-3)
                            .. (-4.8,0) .. (-2,3)
                            .. (0,2) .. (0.4,1) -- (2,3);
                \clip (2,-3) .. (0,-2) .. (-0.4,-1)
                            .. (0,0) .. (0.4,1)
                            .. (0,2) .. (-2,3)
                            .. (-4.8,0) .. (-2,-3)
                            .. (0,-2) .. (0.4,-1) -- (2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}    
            \draw (-2.2,0) node[scale=3] {$+$};
            \draw (2.2,0) node[scale=3] {$-$};
            \draw (5,2.5) node[scale=3] {$M$};
        \end{pgfonlayer}
        \begin{knot}[
                        consider self intersections,
                        clip width=10,
                        clip radius=0.5cm,
                        ignore endpoint intersections=false,
                        flip crossing/.list={6,14}
                    ]
            \strand[very thick,black,closed] 
                    (0.4,1) .. (0,2) .. (-2,3) 
                            .. (-4.8,0) .. (-2,-3) 
                            .. (0,-2) .. (0.4,-1) 
                            .. (0,0) .. (-0.4,1) 
                            .. (0,2) .. (2,3) 
                            .. (4.8,0) .. (2,-3) 
                            .. (0,-2) .. (-0.4,-1) .. (0,0);
        \end{knot}
    \end{tikzpicture}}
\end{document}

enter image description here You can evidently see that even though they are drawn by the same TikZ codes, the results are not the same.

Could someone explain what's going on and how to fix this? Thanks in advance.

Jinwen
  • 8,518

1 Answers1

1

On a bit further testing, this isn't a bug in either hobby or knots, but is a "feature" of their interaction which should probably be documented somewhere.

To create a closed hobby curve, one can type:

\draw[closed] (0,0) .. (1,1) .. (0,2) .. (-1,1) .. (0,0);

and the closed indicates that the curve is to be considered as a closed curve rather than a curve whose endpoints happen to be at the same point (see the hobby documentation for more on the difference).

Now the option closed on the \draw command is a bit out of place because TikZ doesn't know at that point that a hobby curve is on its way. So we have to store that command and effectively say "apply this to the next hobby curve that you encounter". After all, there could be some non-hobby stuff on the path before the hobby curve is created.

This is normally all well and good, and is the easiest way to specify that a hobby curve should be closed. But sometimes we need finer control, for example if using two hobby constructions in the same path. In this situation we can add the closed option to a coordinate in the hobby path. So any of the following would draw the same path as above:

\draw ([closed]0,0) .. (1,1) .. (0,2) .. (-1,1) .. (0,0);
\draw (0,0) .. ([closed]1,1) .. (0,2) .. (-1,1) .. (0,0);
\draw (0,0) .. (1,1) .. (0,2) .. (-1,1) .. ([closed]0,0);

In the second and third of these, TikZ knows that it is constructing a hobby path and so can say "Right, we'll close this path.". In the first, the hobby construction hasn't been triggered (which happens at the first ..) so that closed is still "Apply this to the next hobby path.". But that's a small technicality.

Once options are applied to a hobby path, whether from an earlier style setting or from options gathered from the coordinates, then they are cleared (and globally). If something goes wrong with the clearing, then there is a style clear next Hobby path options which can be used to force clear the options.

So hobby is behaving as it should.

Let's turn to knots. When you define a strand, (via \strand), then the path gets used a lot. If nothing else, it gets used to draw the strand itself and then to cut bits out of the strands it goes over (so for each intersection, the strand is redrawn). Things get even worse when the key consider self intersections is used because this necessitates splitting the strand into components and considering each one separately. So the strand gets drawn a lot of times.

Each time it is drawn, it needs to be styled. So \strand stores its style options and reinvokes them each time the strand is drawn. Normally, this is exactly what is needed. But options that affect the construction of the path aren't needed here because the path has already been constructed. Normally, this doesn't matter - such options are usually discarded.

But not the closed option for hobby paths. Because this is a delayed action command, it hangs around waiting for the next hobby path. The hobby path it was meant for has been constructed and has been dealt with. So it waits, very patiently, for the next one. And as this next one is not meant to be closed, you get the surprise that you did.

Okay, so to solutions. The simplest, and IMHO best, solution is to shift the closed from the \strand to one of the coordinates.

        \strand[very thick,black] 
                ([closed]0.4,1) .. (0,2) .. (-2,3) 
                        .. (-4.8,0) .. (-2,-3) 
                        .. (0,-2) .. (0.4,-1) 
                        .. (0,0) .. (-0.4,1) 
                        .. (0,2) .. (2,3) 
                        .. (4.8,0) .. (2,-3) 
                        .. (0,-2) .. (-0.4,-1) .. (0,0);

An alternative would be put the key clear next Hobby path options before the path that's getting messed around. This doesn't feel quite so elegant to me but I mention it as an alternative.

One could argue that when a path starts, it should clear the options so that every path starts with a blank slate. However, to make that robust would require hooking in to the TikZ scoping mechanism at a slightly deeper level than I've done so far with the hobby package. So it is something that I will keep in mind if I find I need to hook in for another reason, but for now I will leave that on the back burner.

With all that out of the way, here's my recommended solution:

\documentclass{standalone}
% \url{https://tex.stackexchange.com/q/505080/86}
\usepackage{tikz}
\usetikzlibrary{knots}
\usetikzlibrary{hobby}
\begin{document}
    \definecolor{skyblue}{RGB}{60,120,234}
    \scalebox{0.3}{\begin{tikzpicture}[use Hobby shortcut]
        \pgfdeclarelayer{foreground}
        \pgfsetlayers{main,foreground}
        \begin{pgfonlayer}{foreground}
            \begin{scope}
                \clip (-2,3) .. (0,2) .. (0.4,1)
                            .. (0,0) .. (-0.4,-1)
                            .. (0,-2) .. (2,-3)
                            .. (4.8,0) .. (2,3)
                            .. (0,2) .. (-0.4,1) -- (-2,3);
                \clip (-2,-3) .. (0,-2) .. (0.4,-1)
                            .. (0,0) .. (-0.4,1)
                            .. (0,2) .. (2,3)
                            .. (4.8,0) .. (2,-3)
                            .. (0,-2) .. (-0.4,-1) -- (-2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}
            \begin{scope}
                \clip (2,3) .. (0,2) .. (-0.4,1)
                            .. (0,0) .. (0.4,-1)
                            .. (0,-2) .. (-2,-3)
                            .. (-4.8,0) .. (-2,3)
                            .. (0,2) .. (0.4,1) -- (2,3);
                \clip (2,-3) .. (0,-2) .. (-0.4,-1)
                            .. (0,0) .. (0.4,1)
                            .. (0,2) .. (-2,3)
                            .. (-4.8,0) .. (-2,-3)
                            .. (0,-2) .. (0.4,-1) -- (2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}    
            \draw (-2.2,0) node[scale=3] {$+$};
            \draw (2.2,0) node[scale=3] {$-$};
            \draw (5,2.5) node[scale=3] {$M$};
        \end{pgfonlayer}
        \begin{knot}[
                        consider self intersections,
                        clip width=10,
                        clip radius=0.5cm,
                        ignore endpoint intersections=false,
                        flip crossing/.list={6,14}
                    ]
            \strand[very thick,black]
                    ([closed]0.4,1) .. (0,2) .. (-2,3) 
                            .. (-4.8,0) .. (-2,-3) 
                            .. (0,-2) .. (0.4,-1) 
                            .. (0,0) .. (-0.4,1) 
                            .. (0,2) .. (2,3) 
                            .. (4.8,0) .. (2,-3) 
                            .. (0,-2) .. (-0.4,-1) .. (0,0);
        \end{knot}
    \end{tikzpicture}}

    \scalebox{0.3}{\begin{tikzpicture}[use Hobby shortcut]
        \pgfdeclarelayer{foreground}
        \pgfsetlayers{main,foreground}
        \begin{pgfonlayer}{foreground}
            \begin{scope}
                \clip (-2,3) .. (0,2) .. (0.4,1)
                            .. (0,0) .. (-0.4,-1)
                            .. (0,-2) .. (2,-3)
                            .. (4.8,0) .. (2,3)
                            .. (0,2) .. (-0.4,1) -- (-2,3);
                \clip (-2,-3) .. (0,-2) .. (0.4,-1)
                            .. (0,0) .. (-0.4,1)
                            .. (0,2) .. (2,3)
                            .. (4.8,0) .. (2,-3)
                            .. (0,-2) .. (-0.4,-1) -- (-2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}
            \begin{scope}
                \clip (2,3) .. (0,2) .. (-0.4,1)
                            .. (0,0) .. (0.4,-1)
                            .. (0,-2) .. (-2,-3)
                            .. (-4.8,0) .. (-2,3)
                            .. (0,2) .. (0.4,1) -- (2,3);
                \clip (2,-3) .. (0,-2) .. (-0.4,-1)
                            .. (0,0) .. (0.4,1)
                            .. (0,2) .. (-2,3)
                            .. (-4.8,0) .. (-2,-3)
                            .. (0,-2) .. (0.4,-1) -- (2,-3);  
                \fill[skyblue,opacity=0.2] (-4.8,-3) rectangle (4.8,3);
            \end{scope}    
            \draw (-2.2,0) node[scale=3] {$+$};
            \draw (2.2,0) node[scale=3] {$-$};
            \draw (5,2.5) node[scale=3] {$M$};
        \end{pgfonlayer}
        \begin{knot}[
                        consider self intersections,
                        clip width=10,
                        clip radius=0.5cm,
                        ignore endpoint intersections=false,
                        flip crossing/.list={6,14}
                    ]
            \strand[very thick,black] 
                    ([closed]0.4,1) .. (0,2) .. (-2,3) 
                            .. (-4.8,0) .. (-2,-3) 
                            .. (0,-2) .. (0.4,-1) 
                            .. (0,0) .. (-0.4,1) 
                            .. (0,2) .. (2,3) 
                            .. (4.8,0) .. (2,-3) 
                            .. (0,-2) .. (-0.4,-1) .. (0,0);
        \end{knot}
    \end{tikzpicture}}
\end{document}

Correct use of hobby and knots

Andrew Stacey
  • 153,724
  • 43
  • 389
  • 751
  • 1
    No, I said it was a bug in a comment on your post. There's no need to delete your answer - it was helpful in figuring out what was going on. – Andrew Stacey Aug 22 '19 at 17:20
  • Your answer is much better than what I was doing. These are great libraries! –  Aug 22 '19 at 17:31
  • 1
    Thank you both, the solution works perfectly and I'm really impressed for your fast actions. – Jinwen Aug 23 '19 at 03:20