tikz \foreach (senhorita?) se comportando

tikz \foreach (senhorita?) se comportando

No código abaixo, esperava que os 3 casos (comentados) fossem totalmente equivalentes. Infelizmente (para mim) o terceiro produz uma linha reta da coordenada (X) à coordenada (Y). Sempre pensei que a \foreachinstrução deveria apenas inserir o código entre colchetes no fluxo de entrada 'como está', mas não parece ser o caso.

o que estou perdendo? ou isso é um bug (conhecido)?

\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}

Editar: O uso de nós é intencional, para lidar com o tamanho do arco. Além disso, meu caso de uso real está no escopo do pacote tikz cruzamentos. Nesse caso, preciso usar um foreach para lidar com as muitas interseções de caminhos, e então o terceiro caso seria a construção mais natural.

Editar 2: Do argumento nos comentários e da única resposta (por enquanto). Não preciso de formas alternativas de conseguir isso. Já resolvi meu caso da vida real (por meio da implantação de uma estratégia baseada no caso (2)). Minha única pergunta é: O que está acontecendo com o \foreach no terceiro caso? Por que está se comportando assim? Essa é a única questão, não diferentes, formas alternativas de evitar sua armadilha.

Edição 3: Muito obrigado pela resposta de Andrew Stacey que foi além da minha pergunta original. Quer dizer, minha única pergunta era: se o que estava acontecendo era esperado? Agora está claro que é um bug, dada a forma como o foreach é implementado e interage com o subsistema path. Foreach faz mais do que apenas expandir o código entre colchetes substituindo algumas variáveis ​​(as foreach).

Responder1

Como Skillmon diz em seu comentário sobre a resposta, este é um problema de agrupamento. Como está documentado e explorado em várias questões neste site, \foreachcoloca seu código em um grupo (se não me falha a memória, na verdade são dois grupos de profundidade), então qualquer coisa que precise ser lembrada de um loop para o próximo precisa de um pouco de trabalho para tirá-lo desse agrupamento. No nível do usuário, isso geralmente é feito usando a rememberchave, que Qrrbrbirlbel menciona em um de seus comentários sobre sua pergunta.

No entanto, dentro de um caminho, lembrar a coordenada anterior é algo tão útil e comum que o TikZ faz um esforço considerável (literalmente) para garantir que ela seja lembrada corretamente. Portanto, o TikZ instala algum código no início e no final de cada iteração do loop para lembrar o(s) último(s) ponto(s) como se os agrupamentos não estivessem lá. O código que faz isso é relativamente longo:

\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}%

Para referência, na versão atual do TikZ, isso começa na linha 2582 do tikz.code.tex.

Agora, quando o TikZ encontra um nó sem âncora em um caminho, ele insere uma quebra no caminho adicionando um movetoao caminho. O alvo disso movetonão é conhecido no ponto onde é encontrado, pois escolherá o ponto de saída no limite do nó que é mais adequado para a próxima coordenada. Portanto, ele precisa registrar que movetonão foi concluído e que, quando o TikZ souber a próxima coordenada, ele a concluirá movetoantes de descobrir o restante da próxima parte do caminho. Para sinalizar que isso precisa acontecer, o TikZ define uma macro \tikz@moveto@waitinge esta macro contém o nome do nó que o TikZ precisa para completar o cálculo.

Se você olhar atentamente para o \tikz@foreachcódigo que postei acima, verá uma clara falta de referência, \tikz@moveto@waitingo que significa que estenãoseja atualizado no final de cada iteração. Isso significa que cada vez que o TikZ inicia uma iteração, \tikz@moveto@waitingmantém seu valor original fora do loop. No seu caso, esse é o primeiro nó Xe é por isso que todas as suas linhas começam Xe não no nó anterior.

Felizmente, adicionar as linhas necessárias não é difícil. Aqui está uma versão corrigida de \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}%

Código testável:

\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}

A saída do código acima, mostrando três versões de um diagrama que conecta três nós. No primeiro, os três nós são conectados manualmente na ordem correta. No segundo, o código foreach original conecta o segundo e o terceiro ao primeiro. No terceiro, o código foreach corrigido os conecta corretamente novamente.


Atualização: Qrrbrbirlbel observou (em um comentário agora excluído) que ainda havia um problema com o uso to, e o problema do github estava vinculado aOpção coordenar [turn] no foreacho que levanta o mesmo problema com turn. Aqui está um código que pretende corrigir tudo isso e possivelmente torna um pouco mais simples adicionar mais coisas que precisam ser salvas.

\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}

Responder2

Isenção de responsabilidade:Esta não é uma resposta real no sentido de que resolve algum problema, apenas ilustra apor quedeste problema com mais precisão do que a questão (sem qualquer análise de código).

Em seu uso, \foreacho ponto de referência para o próximo segmento de linha não será alterado; portanto, você não está desenhando "apenas a linha para (Y)", mas em vez disso, está desenhando três linhas, (X) -- (nA), (nA) -- (nB), e (nB) -- (Y). A questão é que os pontos para evitar colisões de nós (não tenho ideia de qual é o termo correto em TikZ/ pgfé e com preguiça de procurar) não são recalculados, então você está desenhando da extremidade inferior esquerda (nA)para a extremidade inferior esquerda de (nB), cruzando seu semicírculo.

Ainda assim, \foreacho empate pode ter efeitos colaterais estranhos. Se seus pontos inicial e final não forem coordinates, mas também nodes, o ponto de referência para o início da próxima linha não será movido e você terminará com três linhas com ponto inicial idêntico.

No geral, eu evitaria completamente o seu terceiro uso, parece bastante imprevisível qual será o resultado. Não tenho ideia se isso é um bug!

\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}

insira a descrição da imagem aqui


Como você tem uma solução de força bruta para isso, talvez eu ainda possa fornecer uma solução mais bonita (que não use TikZ, mas outros meios). O seguinte fornece resultados corretos sem a necessidade de especificar uma lista de pares de coordenadas.

\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}

insira a descrição da imagem aqui

informação relacionada