pgf icon indicating copy to clipboard operation
pgf copied to clipboard

Line-to draws differently after swapping points

Open muzimuzhi opened this issue 4 months ago • 7 comments

Brief outline of the bug

In https://github.com/pgf-tikz/pgf/issues/1407#issuecomment-3218376083, @cfr42 found that

\draw (a) -- ([shift={(-500mm,-500mm)}]b);

and

\draw ([shift={(-500mm,-500mm)}]b) -- (a);

draws differently.

The \draw ([shift={(-500mm,-500mm)}]b) -- (a); uses a different end point.

Minimal working example (MWE)

\documentclass[tikz,border=4pt]{standalone}

\begin{document}

\begin{tikzpicture}[draw opacity=.3]
  \node [circle,draw] (a) at (0,0) {A};
  \node [circle,draw] (b) at (1,1) {B};

  \draw[line width=10pt] (a) -- (b);
  \draw[red, line width=7pt] (a) -- ([shift={(-500mm,-500mm)}]b);
  \draw[blue, line width=3pt] ([shift={(-500mm,-500mm)}]b) -- (a);
\end{tikzpicture}

\end{document}
Image

muzimuzhi avatar Aug 24 '25 22:08 muzimuzhi

The problem stems from the \tikz@last@position set by move-to.

Currently it always respects coordinate transformations specified as coordinate options, like the shift=... in ([shift={(-500mm,-500mm)}]b). But compared to other path operations which respect shape border, e.g. line-to, such transformations should be ignored if the move-to point is a shape border.

[!CAUTION] Not always. The current \tikz@@moveto in tikz.code.tex is needed if the next path operation doesn't support border point, e.g. parabola; while my proposed \tikz@@moveto@NEW in the example below is needed if the next path operation supports border point, which is one of --, -|, |-, and ... If the next path operation is another move-to, both works.

As long as we use a one-pass \path parser, \tikz@@moveto may need to save two versions of last coordinate (\tikz@lastx and \tikz@lastx).

I seem to remember that there were discussion on \tikz@last@position/\tikz@lastx/\tikz@lasty, but didn't found the record yet.

\documentclass{article}
\usepackage{tikz}

\makeatletter
\def\tikz@@moveto@NEW#1{%
  \iftikz@shapeborder%
    % ok, target is a shape. recalculate end
    \pgf@process{\pgfpointanchor{\tikz@shapeborder@name}{center}}%
    \tikz@make@last@position{\pgfqpoint{\pgf@x}{\pgf@y}}%
    % ok, the moveto will have to wait. flag that we have a moveto in
    % waiting:
    \edef\tikz@moveto@waiting{\tikz@shapeborder@name}%
  \else%
    \tikz@make@last@position{#1}%
    \tikz@@movetosave{\tikz@last@position}%
    \let\tikz@moveto@waiting=\relax%
  \fi%
  \tikz@scan@next@command%
}%
\makeatother

\NewDocumentCommand{\tests}{}{
\begin{tikzpicture}
  \node [circle,draw] (a) at (0,0) {A};
  \node [circle,draw] (b) at (1,1) {B};

  \begin{scope}[draw opacity=.3]
    \draw[line width=10pt] (a) -- (b);
    \draw[red, line width=7pt] (a) -- ([shift={(-500mm,-500mm)}]b);
    \draw[blue, line width=3pt] ([shift={(-500mm,-500mm)}]b) -- (a);
  \end{scope}
  
  \draw ([shift={(-5mm,0mm)}]b) -- (a);
  \draw ([shift={(-50mm,0mm)}]b) -- (a);
  \draw ([shift={(-500mm,0mm)}]b) -- (a);  
\end{tikzpicture}
\quad
\begin{tikzpicture}
  \node [circle,draw] (a) at (0,0) {A};
  \node [circle,draw] (b) at (1,1) {B};

  \draw (a) parabola (b);
  \draw (b) parabola (a);
  \draw[red]  (a) parabola ([shift={(-5mm,-5mm)}]b);
  \draw[blue] ([shift={(-5mm,-5mm)}]b) parabola (a);
\end{tikzpicture}
}

\begin{document}

Before
\tests

\makeatletter
\let\tikz@@moveto=\tikz@@moveto@NEW
\makeatother

After
\tests
\end{document}
Image

muzimuzhi avatar Aug 24 '25 22:08 muzimuzhi

Thanks. I doubt this is the fix the OP hoped for, but it was the inconsistency which made me think there had to be a bug.

cfr42 avatar Aug 24 '25 23:08 cfr42

Err my \tikz@@moveto@NEW in https://github.com/pgf-tikz/pgf/issues/1409#issuecomment-3218420881 only works if the next path operation supports border point. See update of that comment.

muzimuzhi avatar Aug 25 '25 01:08 muzimuzhi

Err my \tikz@@moveto@NEW in #1409 (comment) only works if the next path operation supports border point. See update of that comment.

@muzimuzhi Thanks to extend another issues from #1407.

I was not so familiar with \tikz@@moveto, but after reading the previous issues comment, I have a intuituion with \tikz@@moveto and \tikz@@moveto@NEW.

As in the code's comment: With normal \tikz@@moveto's behaviour:

% coordinate transformations specified as coordinate options
%   ([<options>]x) -> if path operation supports border point, ignore options 
%                     and use border point ;
%                     otherwise interpreted the same as ([<options>]x.center)

Is that with new version of \tikz@@moveto@NEW, the things flipped? That is:

  • the path operation such as parabola, now in turn, will ignore option and use border point, that cause the inconsisitency
  • the path operation such as --, |-, -| and controls .., now in turn will act as "interpreted the same as ([<options>]x.center)"

If so, that is so funny.

I doubt this is the fix the OP hoped for.

As the caution block shown above, patch the behavior of \tikz@@moveto is little dangerous, I will not apply that in my daily document. But the story above is impressing, that paraphase the in-asymmetric of path operation when shift on node.

IMHO, as user perspective, I need to know what have mentioned that two kinds of path operations (one for parabola, another for ---family), that treat node's shift differently, and that maybe cause not we intuitively expected or naively hoped result. I have to be carefully when use shift with node(as coordinate).

Thanks for your kindness paraphase with detailed example.

Last, one advice on the example, in "After" case:

\begin{tikzpicture}
  \node [circle,draw] (a) at (0,0) {A};
  \node [circle,draw] (b) at (1,1) {B};

  \draw (a) parabola (b);
  \draw (b) parabola (a); % <-this
  \draw[red]  (a) parabola ([shift={(-5mm,-5mm)}]b);
  \draw[blue] ([shift={(-5mm,-5mm)}]b) parabola (a); % <-this
\end{tikzpicture}

The parabola paths pointed out above is the same(for parabola ignored the [shift={(-5mm,-5mm)}] after patch). Maybe add line width will make it more clearly that they are the same path.

\begin{tikzpicture}[draw opacity=.3]
  \node [circle,draw] (a) at (0,0) {A};
  \node [circle,draw] (b) at (1,1) {B};

  \draw[line width=5pt] (a) parabola (b);
  \draw[line width=5pt] (b) parabola (a);
  \draw[red, line width=3pt]  (a) parabola ([shift={(-5mm,-5mm)}]b);
  \draw[blue, line width=1pt] ([shift={(-5mm,-5mm)}]b) parabola (a);
\end{tikzpicture}
Image

Best wishes!

Explorer-cc avatar Aug 26 '25 17:08 Explorer-cc

Ops, I forget my one last vague point in the example...

That is the "Before" Part:

  \draw ([shift={(-5mm,0mm)}]b) -- (a);
  \draw ([shift={(-50mm,0mm)}]b) -- (a);
  \draw ([shift={(-500mm,0mm)}]b) -- (a);  

gives three sloped black line:

Image

As the comment that in #1407 :

 % coordinate transformations specified as coordinate options
  %   ([<options>]x) -> if path operation supports border point, ignore options
  %                     and use border point ;
  %                     otherwise interpreted the same as ([<options>]x.center)
  %   ([<options>]x.<anchor>) -> use x.<anchor>, then apply options

  % line-to path operation ("--") which supports point border

Now in my opinion, the [shift={(-5mm,0mm)}] and [shift={(-50mm,0mm)}] and [shift={(-500mm,0mm)}] would all be ignored, as following -- operation supports point B's border, and it will use "B's border point".....

But then why they are drawn three different sloped lines?

Maybe I neglected something......

Explorer-cc avatar Aug 26 '25 18:08 Explorer-cc

Is that with new version of \tikz@@moveto@NEW, the things flipped? That is:

  • the path operation such as parabola, now in turn, will ignore option and use border point, that cause the inconsisitency
  • the path operation such as --, |-, -| and controls .., now in turn will act as "interpreted the same as ([<options>]x.center)"

The part on whether the options are used or ignored is right (indeed flipped), but the part on supporting for computing border points remain unchanged.

Now in my opinion, the [shift={(-5mm,0mm)}] and [shift={(-50mm,0mm)}] and [shift={(-500mm,0mm)}] would all be ignored, as following -- operation supports point B's border, and it will use "B's border point".....

But then why they are drawn three different sloped lines?

\draw (b) -- (1,1) is different from \draw (b) -- (a). In the latter case, two border points need to be computed, so order might matters.

Preparation:

Coordinate transformations given as coordinate options are never ignored when computing the last position. A simple example: \draw ([shift={(-5mm,5mm)}]a) circle (1pt); See more discussion in https://github.com/pgf-tikz/pgf/issues/1407#issuecomment-3225420906.

A border point is constructed by using \pgfpointshapeborder{<node>}{<point>}, which (quoting its doc in pgfmanual)

returns the point on the border of the shape that lies on a straight line from the center of the node to <point>.

If there are multiple intersections, for circle node it looks like the one farther from the node center is returned.

Explanation:

In \tikz@@lineto, when both the start and end points are border points, it first computes border point on the end node, using \pgfpointshapeborder{<end node>}{<last position>}. Then computes border point on the start node, using \pgfpointshapeborder{<start node>}{<border point on end node>}.

Therefore in \draw ([shift={(-5mm,0mm)}]b) -- (a);, when parsing -- (a), since the last position was shifted, the computed start and end border points are both disturbed.

The following example computes the two border points manually in pgf command (the system layer), and shows that the resulting softpath is the same as that auto computed in tikz.

\documentclass{article}
\usepackage{tikz}

\newdimen\pgftempxa
\newdimen\pgftempya

\begin{document}

\begin{tikzpicture}
  \node [circle,draw] (a) at (0,0) {A};
  \node [circle,draw] (b) at (1,1) {B};

  \draw[save path=\mytikzpath] ([shift={(-500mm,0mm)}]b) -- (a);
  \show\mytikzpath
  \draw[use path=\mytikzpath];
\end{tikzpicture}

\begin{pgfpicture}
  \pgfnode{circle}{center}{A}{a}{\pgfusepath{stroke}}
  {
    \pgftransformshift{\pgfpointxy{1}{1}}
    \pgfnode{circle}{center}{B}{b}{\pgfusepath{stroke}}
  }

  % calculate end point, using the shifted a.center
  \pgfpointshapeborder{a}
    {\pgfpointadd{\pgfpointanchor{b}{center}}{\pgfqpoint{-500mm}{0mm}}}
  \pgfgetlastxy{\pgftempxa}{\pgftempya}
  % calculate and move to start point
  \pgfpathmoveto{\pgfpointshapeborder{b}{\pgfqpoint{\pgftempxa}{\pgftempya}}}
  % line to end point
  \pgfpathlineto{\pgfqpoint{\pgftempxa}{\pgftempya}}

  \makeatletter
  \pgfsyssoftpath@getcurrentpath\mypgfpath
  \makeatother
  \show\mypgfpath

  \pgfusepath{stroke}
\end{pgfpicture}

\end{document}

in log

> \mytikzpath=macro:
->\pgfsyssoftpath@movetotoken {20.54555pt}{22.63548pt}\pgfsyssoftpath@linetotok
en {-9.94609pt}{0.2028pt}.
l.16   \show\mytikzpath
                       

> \mypgfpath=macro:
->\pgfsyssoftpath@movetotoken {20.54555pt}{22.63548pt}\pgfsyssoftpath@linetotok
en {-9.94609pt}{0.2028pt}.
l.39   \show\mypgfpath

One may further ask: why not simply use <start node>.center and <end node>.center in \pgfpointshapeborder? I don't know the answer. I can only try explaining what the current code does, but not why it's designed/implemented in this way.

muzimuzhi avatar Aug 26 '25 21:08 muzimuzhi

As long as we use a one-pass \path parser, \tikz@@moveto may need to save two versions of last coordinate (\tikz@lastx and \tikz@lastx).

There is already a marker: \tikz@moveto@waiting. If the move-to point is simply a node name, \tikz@moveto@waiting holds the name; otherwise it's let to \relax.

\tikz@moveto@waiting is already used as the marker in \tikz@flush@moveto and \tikz@flush@moveto@toward.

One may further ask: why not simply use <start node>.center and <end node>.center in \pgfpointshapeborder? I don't know the answer. I can only try explaining what the current code does, but not why it's designed/implemented in this way.

My attempts, both the first (\tikz@@moveto@NEW) and the following second one, do want to achieve such result.

\documentclass{article}
\usepackage{tikz}

% TODO: patch \tikz@@hv@lineto, \tikz@@vh@lineto, and \tikz@curveC similarly

\makeatletter
\def\tikz@make@last@position@with@moveto@waiting{%
  \ifx\tikz@moveto@waiting\relax
  \else
    % \tikz@moveto@waiting holds a node name
    \tikz@make@last@position{\pgfpointanchor{\tikz@moveto@waiting}{center}}%
  \fi
}

\def\tikz@@lineto@NEW#1{%
  % <<< patch begin
  % start point is a shape, recalculate the last position
  \tikz@make@last@position@with@moveto@waiting
  % >>> patch end
  % Record the starting point for later labels on the path:
  \edef\tikz@timer@start{\noexpand\pgfqpoint{\the\tikz@lastx}{\the\tikz@lasty}}%
  \iftikz@shapeborder%
    % ok, target is a shape. recalculate end
    \pgf@process{\pgfpointshapeborder{\tikz@shapeborder@name}{\tikz@last@position}}%
    \tikz@make@last@position{\pgfqpoint{\pgf@x}{\pgf@y}}%
    \tikz@flush@moveto@toward{\tikz@last@position}\pgf@x\pgf@y%
    \tikz@path@lineto{\tikz@last@position}%
    \edef\tikz@timer@end{\noexpand\pgfqpoint{\the\tikz@lastx}{\the\tikz@lasty}}%
    \tikz@make@last@position{#1}%
    \edef\tikz@moveto@waiting{\tikz@shapeborder@name}%
  \else%
    % target is a reasonable point...
    % Record the starting point for later labels on the path:
    \tikz@make@last@position{#1}%
    \tikz@flush@moveto@toward{\tikz@last@position}\pgf@x\pgf@y%
    \tikz@path@lineto{\tikz@last@position}%
    \edef\tikz@timer@end{\noexpand\pgfqpoint{\the\tikz@lastx}{\the\tikz@lasty}}%
  \fi%
  \let\tikz@timer=\tikz@timer@line%
  \let\tikz@tangent\tikz@timer@start%
  \tikz@scan@next@command%
}%
\makeatother

\NewDocumentCommand{\tests}{}{
\begin{tikzpicture}[nodes={circle, draw}]
  \node (a) at (0,0) {A};
  \node (b) at (1,1) {B};

  \begin{scope}[draw opacity=.3]
    % test (node) -- (node), (node) -- (x,y)
    \draw[line width=10pt]
      (a) -- (b) (b) -- (2,0);
    % test (node) -- ([...]node), (x,y) -- ([...]node)
    \draw[red, line width=7pt]
      (a) -- ([shift={(-500mm,-500mm)}]b) (2,0) -- ([shift={(-500mm,-500mm)}]b);
    % test ([...]node) -- (node), ([...]node) -- (x,y)
    \draw[blue, line width=3pt]
      ([shift={(-500mm,-500mm)}]b) -- (a) ([shift={(-500mm,-500mm)}]b) -- (2,0);
    % test ([...]node) -- ([...]node)
    \draw
      ([shift={(-500mm,-500mm)}]b) -- ([shift={(500mm,500mm)}]a);
  \end{scope}

  % test ([...]node) -- (node), ([...]node) -- (x,y), variants
  \draw ([shift={(-5mm,0mm)}]b) -- (a) ([shift={(-5mm,0mm)}]b) -- (2,0);
  \draw ([shift={(-50mm,0mm)}]b) -- (a) ([shift={(-50mm,0mm)}]b) -- (2,0);
  \draw ([shift={(-500mm,0mm)}]b) -- (a) ([shift={(-500mm,0mm)}]b) -- (2,0);
\end{tikzpicture}
\quad
\begin{tikzpicture}
  \node [circle,draw] (a) at (0,0) {A};
  \node [circle,draw] (b) at (1,1) {B};

  \begin{scope}[draw opacity=.3]
    % test (node) parabola (node), (node) parabola (x,y), (x,y) parabola (node)
    \draw[line width=5pt]
      (a) parabola (b) (b) parabola (2,0);
    \draw[green, line width=5pt]
      (b) parabola (a) (2,0) parabola (b);
    % test (node) parabola ([...]node), (x,y) parabola ([...]node)
    \draw[red, line width=3pt]
      (a) parabola ([shift={(-5mm,-5mm)}]b) (2,0) parabola ([shift={(-5mm,-5mm)}]b);
    % test ([...]node) parabola (node), ([...]node) parabola (x,y)
    \draw[blue, line width=1pt]
      ([shift={(-5mm,-5mm)}]b) parabola (a) ([shift={(-5mm,-5mm)}]b) parabola (2,0);
  \end{scope}
\end{tikzpicture}
}

\begin{document}

Before
\tests

\makeatletter
\let\tikz@@lineto=\tikz@@lineto@NEW
\makeatother

After
\tests

\end{document}
Image

Update 1: Moved \tikz@make@last@position@with@moveto@waiting to the very first in \tikz@@lineto@NEW. Extended tests involving (x,y) canvas coordinate.

muzimuzhi avatar Aug 27 '25 21:08 muzimuzhi