tikz \foreach (пропустить?) ведет себя

tikz \foreach (пропустить?) ведет себя

В коде ниже я ожидал, что 3 случая (закомментированные) будут полностью эквивалентны. К сожалению (для меня) 3-й случай создает прямую линию от координаты (X) до координаты (Y). Я всегда думал, что оператор \foreachпросто вставляет код между фигурными скобками во входной поток «как есть», но, похоже, это не так.

Что я упускаю? Или это (известная) ошибка?

\documentclass{article}
\usepackage{tikz}

\begin{document}

\begin{tikzpicture}
  \path 
    (0,0) coordinate(X) 
    (1,1) coordinate(A) node[minimum size=4.3pt,inner sep=0pt](nA){}
    (2,2) coordinate(B) node[minimum size=4.3pt,inner sep=0pt](nB){}
    (3,3) coordinate(Y)
    ;
  \draw[rotate=45] 
    (A) +(-7pt/2,0) arc[start angle=180,end angle=0,radius=7pt/2]
    (B) +(-7pt/2,0) arc[start angle=180,end angle=0,radius=7pt/2]
    ;
    
    % Case (1)
  %\draw (X) -- (nA) -- (nB) -- (Y) ;
  
    % Case (2)
  %\foreach \x/\y in {X/nA,nA/nB,nB/Y} {\draw (\x) -- (\y);}

    % Case (3)
  %\draw (X) \foreach \x in {nA,nB,Y} { -- (\x)};
  
\end{tikzpicture}

\end{document}

Редактировать: использование узлов намеренное, чтобы справиться с размером дуги. Кроме того, мой реальный вариант использования находится в области действия пакета пересечений tikz. В этом случае мне нужно использовать foreach для работы со многими пересечениями путей, и тогда 3-й случай будет наиболее естественной конструкцией.

Редактирование 2: Из аргумента в комментариях и (пока) единственного ответа. Мне не нужны альтернативные способы достижения этого. Я уже решил свой реальный случай (путем развертывания стратегии, основанной на случае (2)). Мой единственный вопрос: что происходит с \foreach в 3-м случае? Почему он так себя ведет? Это единственный вопрос, а не другие, альтернативные способы избежать его ловушки.

Редактирование 3: Большое спасибо за ответ Эндрю Стейси, который превзошел мой первоначальный вопрос. Я имею в виду, что мой единственный вопрос был: если то, что происходило, ожидалось? Теперь ясно, что это ошибка, учитывая способ реализации foreach и взаимодействия с подсистемой путей. Foreach делает больше, чем просто расширяет код в фигурных скобках, заменяя некоторые переменные (foreach).

решение1

Как говорит Skillmon в своем комментарии к своему ответу, это проблема группировки. Как задокументировано и исследовано в различных вопросах на этом сайте, \foreachпомещает свой код в группу (если мне не изменяет память, то на самом деле это две группы глубиной), поэтому все, что нужно запомнить от одного цикла до следующего, требует некоторой работы, чтобы вытащить его из этой группировки. На уровне пользователя это обычно достигается с помощью ключа remember, который Qrrbrbirlbel упоминает в одном из своих комментариев к вашему вопросу.

Однако внутри пути запоминание предыдущей координаты является настолько полезным и обычным делом, что TikZ идет на значительные меры (буквально), чтобы гарантировать, что она запомнена правильно. Поэтому TikZ устанавливает некоторый код в начале и конце каждой итерации цикла, чтобы запомнить последнюю точку(ы), как будто группировок там не было. Код, который это делает, относительно длинный:

\def\tikz@foreach{%
  \def\pgffor@beginhook{%
    \tikz@lastx=\tikz@foreach@save@lastx%
    \tikz@lasty=\tikz@foreach@save@lasty%
    \tikz@lastxsaved=\tikz@foreach@save@lastxsaved%
    \tikz@lastysaved=\tikz@foreach@save@lastysaved%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \expandafter\tikz@scan@next@command\pgfutil@firstofone}%
  \def\pgffor@endhook{\pgfextra{%
      \xdef\tikz@foreach@save@lastx{\the\tikz@lastx}%
      \xdef\tikz@foreach@save@lasty{\the\tikz@lasty}%
      \xdef\tikz@foreach@save@lastxsaved{\the\tikz@lastxsaved}%
      \xdef\tikz@foreach@save@lastysaved{\the\tikz@lastysaved}%
      \global\setbox\tikz@tempbox=\box\tikz@figbox%
      \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
      \pgfutil@gobble}}%
  \def\pgffor@afterhook{%
    \tikz@lastx=\tikz@foreach@save@lastx%
    \tikz@lasty=\tikz@foreach@save@lasty%
    \tikz@lastxsaved=\tikz@foreach@save@lastxsaved%
    \tikz@lastysaved=\tikz@foreach@save@lastysaved%
    \let\pgffor@beginhook\relax%
    \let\pgffor@endhook\relax%
    \let\pgffor@afterhook\relax%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \tikz@scan@next@command}%
  \global\setbox\tikz@tempbox=\box\tikz@figbox%
  \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
  \xdef\tikz@foreach@save@lastx{\the\tikz@lastx}%
  \xdef\tikz@foreach@save@lasty{\the\tikz@lasty}%
  \xdef\tikz@foreach@save@lastxsaved{\the\tikz@lastxsaved}%
  \xdef\tikz@foreach@save@lastysaved{\the\tikz@lastysaved}%
  \foreach}%

Для справки: в текущей версии TikZ это начинается на строке 2582 файла tikz.code.tex.

Теперь, когда TikZ встречает узел без якоря на пути, он вставляет разрыв в путь, добавляя movetoк пути. Однако цель этого movetoнеизвестна в точке, где он встречается, поскольку он выберет точку выхода на границе узла, которая наиболее подходит для следующей координаты. Поэтому ему нужно записать, что movetoне было завершено, и что когда TikZ узнает следующую координату, он завершит ее movetoдо того, как вычислит остальную часть следующей части пути. Чтобы отметить, что это должно произойти, TikZ определяет макрос \tikz@moveto@waiting, и этот макрос содержит имя узла, которое необходимо TikZ для завершения расчета.

Если вы внимательно посмотрите на \tikz@foreachкод, который я разместил выше, то вы увидите явное отсутствие ссылки на, \tikz@moveto@waitingчто означает, что этоне делаетобновляются в конце каждой итерации. Это означает, что каждый раз, когда TikZ начинает итерацию, \tikz@moveto@waitingсохраняет свое исходное значение извне цикла. В вашем случае это первый узел, Xи именно поэтому все ваши строки начинаются с X, а не с предыдущего узла.

К счастью, добавить необходимые строки несложно. Вот пропатченная версия \tikz@foreach:

\def\tikz@foreach{%
  \def\pgffor@beginhook{%
    \global\let\tikz@moveto@waiting\tikz@foreach@moveto@waiting%
    \tikz@lastx=\tikz@foreach@save@lastx%
    \tikz@lasty=\tikz@foreach@save@lasty%
    \tikz@lastxsaved=\tikz@foreach@save@lastxsaved%
    \tikz@lastysaved=\tikz@foreach@save@lastysaved%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \expandafter\tikz@scan@next@command\pgfutil@firstofone}%
  \def\pgffor@endhook{\pgfextra{%
      \global\let\tikz@foreach@moveto@waiting\tikz@moveto@waiting%
      \xdef\tikz@foreach@save@lastx{\the\tikz@lastx}%
      \xdef\tikz@foreach@save@lasty{\the\tikz@lasty}%
      \xdef\tikz@foreach@save@lastxsaved{\the\tikz@lastxsaved}%
      \xdef\tikz@foreach@save@lastysaved{\the\tikz@lastysaved}%
      \global\setbox\tikz@tempbox=\box\tikz@figbox%
      \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
      \pgfutil@gobble}}%
  \def\pgffor@afterhook{%
    \let\tikz@moveto@waiting\tikz@foreach@moveto@waiting%
    \tikz@lastx=\tikz@foreach@save@lastx%
    \tikz@lasty=\tikz@foreach@save@lasty%
    \tikz@lastxsaved=\tikz@foreach@save@lastxsaved%
    \tikz@lastysaved=\tikz@foreach@save@lastysaved%
    \let\pgffor@beginhook\relax%
    \let\pgffor@endhook\relax%
    \let\pgffor@afterhook\relax%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \tikz@scan@next@command}%
  \global\setbox\tikz@tempbox=\box\tikz@figbox%
  \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
  \xdef\tikz@foreach@save@lastx{\the\tikz@lastx}%
  \xdef\tikz@foreach@save@lasty{\the\tikz@lasty}%
  \xdef\tikz@foreach@save@lastxsaved{\the\tikz@lastxsaved}%
  \xdef\tikz@foreach@save@lastysaved{\the\tikz@lastysaved}%
  \global\let\tikz@foreach@moveto@waiting\tikz@moveto@waiting%
  \foreach}%

Тестируемый код:

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

\makeatletter
\def\tikz@patched@foreach{%
  \def\pgffor@beginhook{%
    \global\let\tikz@moveto@waiting\tikz@foreach@moveto@waiting%
    \tikz@lastx=\tikz@foreach@save@lastx%
    \tikz@lasty=\tikz@foreach@save@lasty%
    \tikz@lastxsaved=\tikz@foreach@save@lastxsaved%
    \tikz@lastysaved=\tikz@foreach@save@lastysaved%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \expandafter\tikz@scan@next@command\pgfutil@firstofone}%
  \def\pgffor@endhook{\pgfextra{%
      \global\let\tikz@foreach@moveto@waiting\tikz@moveto@waiting%
      \xdef\tikz@foreach@save@lastx{\the\tikz@lastx}%
      \xdef\tikz@foreach@save@lasty{\the\tikz@lasty}%
      \xdef\tikz@foreach@save@lastxsaved{\the\tikz@lastxsaved}%
      \xdef\tikz@foreach@save@lastysaved{\the\tikz@lastysaved}%
      \global\setbox\tikz@tempbox=\box\tikz@figbox%
      \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
      \pgfutil@gobble}}%
  \def\pgffor@afterhook{%
    \let\tikz@moveto@waiting\tikz@foreach@moveto@waiting%
    \tikz@lastx=\tikz@foreach@save@lastx%
    \tikz@lasty=\tikz@foreach@save@lasty%
    \tikz@lastxsaved=\tikz@foreach@save@lastxsaved%
    \tikz@lastysaved=\tikz@foreach@save@lastysaved%
    \let\pgffor@beginhook\relax%
    \let\pgffor@endhook\relax%
    \let\pgffor@afterhook\relax%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \tikz@scan@next@command}%
  \global\setbox\tikz@tempbox=\box\tikz@figbox%
  \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
  \xdef\tikz@foreach@save@lastx{\the\tikz@lastx}%
  \xdef\tikz@foreach@save@lasty{\the\tikz@lasty}%
  \xdef\tikz@foreach@save@lastxsaved{\the\tikz@lastxsaved}%
  \xdef\tikz@foreach@save@lastysaved{\the\tikz@lastysaved}%
  \global\let\tikz@foreach@moveto@waiting\tikz@moveto@waiting%
  \foreach}%

\DeclareDocumentCommand \FixForeach {}
{
  \let\tikz@foreach=\tikz@patched@foreach
}
\makeatother

\begin{document}

\begin{tikzpicture}
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) -- (B) -- (C);
\end{tikzpicture}

\begin{tikzpicture}
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) \foreach \nd in {B,C} { -- (\nd) };
\end{tikzpicture}

\FixForeach

\begin{tikzpicture}
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) \foreach \nd in {B,C} { -- (\nd) };
\end{tikzpicture}


\end{document}

Вывод кода выше, показывающий три версии диаграммы, которая соединяет три узла. В первой три узла вручную соединены в правильном порядке. Во второй исходный код foreach соединяет второй и третий с первым. В третьей исправленный код foreach снова соединяет их правильно.


Обновление: Qrrbrbirlbel отметил (в теперь уже удаленном комментарии), что проблема с использованием все еще существует to, а проблема с github связана сКоординировать [turn] опцию в foreachчто вызывает ту же проблему, что и turn. Вот код, который призван исправить все это и, возможно, немного упростить добавление дополнительных данных, которые необходимо сохранить.

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

\makeatletter

\def\pgffor@smugglers@cove{}
\let\tikztostart=\relax

\def\pgffor@smuggle@macro#1{%
  \pgfutil@ifx#1\relax{}{%
    \expandafter
    \pgfutil@g@addto@macro
    \expandafter
    \pgffor@smugglers@cove
    \expandafter
    {%
      \expandafter\def\expandafter#1\expandafter{#1}%
    }%
  }%
}

\def\pgffor@smuggle@dimen#1{%
  \pgfutil@ifx#1\relax{}{%
    \expandafter
    \pgfutil@g@addto@macro
    \expandafter
    \pgffor@smugglers@cove
    \expandafter
    {%
      \expandafter#1\expandafter=\the#1\relax%
    }%
  }%
}

\def\pgffor@smugglers@loot{%
  %
  \pgffor@smuggle@macro\tikz@moveto@waiting%
  \pgffor@smuggle@macro\tikztostart%
  \pgffor@smuggle@macro\tikz@tangent%
  %
  \pgffor@smuggle@dimen\tikz@lastx%
  \pgffor@smuggle@dimen\tikz@lasty%
  \pgffor@smuggle@dimen\tikz@lastxsaved%
  \pgffor@smuggle@dimen\tikz@lastysaved%
  %
}


\def\tikz@patched@foreach{%
  \def\pgffor@beginhook{%
    \pgffor@smugglers@cove
    \gdef\pgffor@smugglers@cove{}%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \expandafter\tikz@scan@next@command\pgfutil@firstofone}%
  \def\pgffor@endhook{\pgfextra{%
      \pgffor@smugglers@loot%
      \global\setbox\tikz@tempbox=\box\tikz@figbox%
      \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
      \pgfutil@gobble}}%
  \def\pgffor@afterhook{%
    \pgffor@smugglers@cove
    \gdef\pgffor@smugglers@cove{}%
    \let\pgffor@beginhook\relax%
    \let\pgffor@endhook\relax%
    \let\pgffor@afterhook\relax%
    \setbox\tikz@figbox=\box\tikz@tempbox%
    \setbox\tikz@figbox@bg=\box\tikz@tempbox@bg%
    \tikz@scan@next@command}%
  \global\setbox\tikz@tempbox=\box\tikz@figbox%
  \global\setbox\tikz@tempbox@bg=\box\tikz@figbox@bg%
  \pgffor@smugglers@loot%
  \foreach}%

\DeclareDocumentCommand \FixForeach {}
{
  \let\tikz@foreach=\tikz@patched@foreach
}
\makeatother

\begin{document}

\begin{tabular}{rl}
Paths: &
\begin{tikzpicture}
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) -- (B) -- (C);
\end{tikzpicture} \\
%
Foreach Paths: &
\begin{tikzpicture}
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) \foreach \nd in {B,C} { -- (\nd) };
\end{tikzpicture} \\
%
Tos: &
\begin{tikzpicture}
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) to (B) to (C);
\end{tikzpicture} \\
%
Foreach Tos: &
\begin{tikzpicture}
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) \foreach \nd in {B,C} { to (\nd) };
\end{tikzpicture} \\
%
Turns: &
\begin{tikzpicture}
  \draw[red] (0,0) -- (1,.2) -- ([turn]90:1cm) -- ([turn]90:1cm) -- ([turn]90:1cm) -- ([turn]90:1cm);
\end{tikzpicture} \\
%
Foreach Turns: &
\begin{tikzpicture}
  \draw (0,0) -- (1,.2) foreach \i in {1,2,3,4} { -- ([turn]90:1cm) };
\end{tikzpicture} \\
%
Fixed Foreach Paths: &
\begin{tikzpicture}
\FixForeach
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) \foreach \nd in {B,C} { -- (\nd) };
\end{tikzpicture} \\
%
Fixed Foreach Tos: &
\begin{tikzpicture}
\FixForeach
\node[draw] (A) at (0,0) {};
\node[draw] (B) at (1,1) {};
\node[draw] (C) at (2,0) {};
\draw (A) \foreach \nd in {B,C} { to (\nd) };
\end{tikzpicture} \\
%
Fixed Foreach Turns: &
\begin{tikzpicture}
\FixForeach
  \draw (0,0) -- (1,.2) foreach \i in {1,2,3,4} { -- ([turn]90:1cm) };
\end{tikzpicture}


\end{tabular}

\end{document}

решение2

Отказ от ответственности:Это не настоящий ответ в том смысле, что он решает какую-то проблему, он только иллюстрируетпочемуэтой проблемы точнее, чем вопрос (без какого-либо анализа кода).

В вашем использовании \foreachне изменится опорная точка для следующего сегмента линии, поэтому вы не рисуете «только линию до (Y)», а вместо этого рисуете три линии, (X) -- (nA), (nA) -- (nB), и (nB) -- (Y). Проблема в том, что точки для избежания столкновений узлов (не знаю, как правильно называть этот термин в TiкZ/ pgf— и слишком лениво, чтобы это выяснить) не пересчитываются, поэтому вы рисуете от нижнего левого конца (nA)до нижнего левого конца (nB), пересекая ваш полукруг.

Тем не менее, \foreachвнутри розыгрыша могут быть странные побочные эффекты. Если ваши начальная и конечная точки не coordinates, а также nodes, точка отсчета для начала следующей линии вообще не перемещается, и вы получаете три линии с одинаковой начальной точкой.

В целом я бы вообще избегал вашего третьего использования, кажется довольно непредсказуемым, какой будет результат. Я понятия не имею, баг ли это!

\documentclass{article}
\usepackage{tikz}

\begin{document}

\begin{tikzpicture}[mynode/.style={minimum size=4.3pt, inner sep=0pt, draw=red}]
  \path
    (0,0) node[mynode](X){}
    (1,0) node[mynode](nA){}
    (1,1) node[mynode](nB){}
    (1,2) node[mynode](Y){}
    ;
    % Case (3)
  \draw (X) \foreach \x in {nA,nB,Y} { -- (\x)};

  \path
    (2,0) coordinate(X)
    (3,0) node[mynode](nA){}
    (3,1) node[mynode](nB){}
    (3,2) coordinate(Y)
    ;
    % Case (3)
  \draw (X) \foreach \x in {nA,nB,Y} { -- (\x)};

  \path
    (4,0) coordinate(X)
    (5,0) coordinate(nA)
    (5,1) coordinate(nB)
    (5,2) coordinate(Y)
    ;
  \draw (X) \foreach \x in {nA,nB,Y} { -- (\x)};
\end{tikzpicture}

\end{document}

введите описание изображения здесь


Поскольку у вас есть решение этой проблемы методом перебора, возможно, я все же смогу предоставить более красивое решение (которое не использует TiкZ, но другие средства). Следующий пример дает правильные результаты без необходимости указывать список пар координат.

\documentclass[border=3.14,tikz]{standalone}

\ExplSyntaxOn
\cs_new_protected:Npn \tocoordinatelist { \exp:w \__tocoordinatelist:w }
\NewExpandableDocumentCommand \__tocoordinatelist:w { O{--} m }
  { \exp_end: #1 ( \clist_use:nn {#2} { ) #1 ( }) }
\ExplSyntaxOff

\begin{document}

\begin{tikzpicture}[mynode/.style={minimum size=4.3pt, inner sep=0pt, draw=red}]
  \path
    (0,0) coordinate(X)
    (1,0) node[mynode](nA){}
    (1,1) node[mynode](nB){}
    (1,2) coordinate(Y)
    ;
  \draw (X) \tocoordinatelist{nA,nB,Y};
\end{tikzpicture}

\end{document}

введите описание изображения здесь

Связанный контент