2

As an attempt to solve my Define a 2D canvas using preexisting points, I came across the idea of @andrew swann's answer to Pass a 3d coordinate via pgfkeys and tried to adapt it.

This is how the 3d coordinates are stored additionally to the 3d coordinate being transformed into 2d coordinates.

    % save 3d coordinates of point
    \def\tdcoord #1 at (#2,#3,#4);{\pgfkeys{/tdcoords/#1/.is
    family,/tdcoords/#1,x/.initial=#2,y/.initial=#3,z/.initial=#4}}

And while everything works fine and i was able to adapt the canvas just giving points defined with an additional \tdcoords, it only works when that call is not in a foreach loop.

The Error messages are

  • Undefined controll sequence
  • Missing number, treated as zero
  • Argument of \pgfmath@dimen@@ has an extra }
  • Paragraph ended before \pgfmath@dimen@@ was complete
  • Extra }, or forgotten \endgroup

Anyone have an idea why or how to fix it?

Adapted MWE:

    \documentclass{standalone}
    \usepackage{tikz}
    \usetikzlibrary{calc}
    \usepackage{tikz-3dplot}
% my adaption attempt
\makeatletter
\tikzoption{canvasP}[]{\@setPOxy#1}
\def\@setPOxy #1,#2,#3%
  {\def\tikz@plane@origin{\pgfpointxyz{\GetX(#1)}{\GetY(#1)}{\GetZ(#1)}}%
   \def\tikz@plane@x{\pgfpointxyz{\GetX(#2)}{\GetY(#2)}{\GetZ(#2)}}%
   \def\tikz@plane@y{\pgfpointxyz{\GetX(#3)}{\GetY(#3)}{\GetZ(#3)}}%
   \tikz@canvas@is@plane}
\makeatother

% save 3d coordinates of point
\def\tdcoord #1 at (#2,#3,#4);{\pgfkeys{/tdcoords/#1/.is
family,/tdcoords/#1,x/.initial=#2,y/.initial=#3,z/.initial=#4}}

% Get the 3d coordinate components of a point
\def\GetX(#1){\pgfkeysvalueof{/tdcoords/(#1)/x}}
\def\GetY(#1){\pgfkeysvalueof{/tdcoords/(#1)/y}}
\def\GetZ(#1){\pgfkeysvalueof{/tdcoords/(#1)/z}}

% Define multiple 3d points following tkz-euclide notation
\newcommand{\tkzDefdPoints}[2][]{
  \foreach \ptx/\pty/\ptz/\name in {#2}{
    \path [#1] (\ptx,\pty,\ptz) coordinate (\name);
    \tdcoord (\name) at (\ptx,\pty,\ptz);
  }
}

\tdplotsetmaincoords{70}{110}

\begin{document}
\begin{tikzpicture}[tdplot_main_coords]
  \draw[->] (0,0,0) -- (5,0,0) node[right]{$x$};
  \draw[->] (0,0,0) -- (0,5,0) node[above]{$y$};
  \draw[->] (0,0,0) -- (0,0,5) node[below left]{$z$};

  \tkzDefdPoints{2/2/2/A,2/3/2/B,2/2/3/C,
                 2/2/2/D,2/3/2/E,2/2/3/F}

  \tdcoord (A) at (2,2,2);
  \tdcoord (B) at (2,3,2);
  \tdcoord (C) at (2,2,3);

  \draw (A) -- (B) -- (C) -- cycle;

  \begin{scope}[canvasP={A,B,C}]
    \draw[red] (1,0) -- (1,1) -- (0,1);
  \end{scope}

  % \begin{scope}[canvasP={D,E,F}]
  %   \draw[blue] (0,1) -- (1,2) -- (2,1) -- (1,0);
  % \end{scope}
\end{tikzpicture}
\end{document}

Update: While renaming points, I just realized that this might have nothing to do with the foreach statement. Changing the coordinate names to something that is not a single letter, like \tdcoord (AX) at (2,2,2); produces the exact same error, even when it is called outside of the foreach loop.

Shakaja
  • 155

1 Answers1

2
  1. \@setPOxy is not delimited, meaning canvasP={AA,BB,CC} will lead to #3 being C and a C lost to TeX.
  2. You're storing \ptx and so on in the value-keys and not the values (this would need /.initial/.expanded).
  3. The body of a \foreach loop is local, all key assignments are lost after the body.

We could solve 1. by using

\tikzset{canvasP/.code=\@setPOxy#1\@stop}
\def\@setPOxy #1,#2,#3\@stop
  {\def\tikz@plane@origin{\pgfpointxyz{\GetX(#1)}{\GetY(#1)}{\GetZ(#1)}}%
   \def\tikz@plane@x{\pgfpointxyz{\GetX(#2)}{\GetY(#2)}{\GetZ(#2)}}%
   \def\tikz@plane@y{\pgfpointxyz{\GetX(#3)}{\GetY(#3)}{\GetZ(#3)}}%
   \tikz@canvas@is@plane}

but since PGFkeys already uses delimited values/arguments we can just use a key directly here:

canvasP/.code args={#1,#2,#3}{%
  \def\tikz@plane@origin{\pgfpointxyz{\GetX(#1)}{\GetY(#1)}{\GetZ(#1)}}%
  \def\tikz@plane@x{\pgfpointxyz{\GetX(#2)}{\GetY(#2)}{\GetZ(#2)}}%
  \def\tikz@plane@y{\pgfpointxyz{\GetX(#3)}{\GetY(#3)}{\GetZ(#3)}}%
  \tikz@canvas@is@plane}

The 2. problem won't be one anymore when we're solving 3.

We need to loop over your list without this being inside a group. (We could also do global definitions similar to another answer of mine with a similar cause but that's not necessary.

We could also use PGfplots' \pgfplotsinvokeforeach but that's not necessary either, we can use the .list handler of PGFkeys:

def dPoints/.style 2 args={%
  @def dPoints/.code args={##1/##2/##3/##4}{%
    \path [#1] (##1,##2,##3) coordinate (##4);
    \tdcoord (##4) at (##1,##2,##3);},
  @def dPoints/.list={#2}
}

This sets up a key @def dPoints that takes four arguments separated by / very similar to your \foreach loop but it does not assign it to a macro nor will it be executed inside a group when we use it with .list. (Internally, .list uses a scoped (i.e. grouped) \foreach loop but it does execute everything after the loop.)

The argument #1 of def dPoints isn't optional anymore (but it can be empty) but since we have it, we need to redefine @def dPoints everytime we want to use it.

Code

\documentclass{standalone}
\usepackage{tikz}
\usetikzlibrary{calc}
\usepackage{tikz-3dplot}

% my adaption attempt \makeatletter \tikzset{ canvasP/.code args={#1,#2,#3}{% \def\tikz@plane@origin{\pgfpointxyz{\GetX(#1)}{\GetY(#1)}{\GetZ(#1)}}% \def\tikz@plane@x{\pgfpointxyz{\GetX(#2)}{\GetY(#2)}{\GetZ(#2)}}% \def\tikz@plane@y{\pgfpointxyz{\GetX(#3)}{\GetY(#3)}{\GetZ(#3)}}% \tikz@canvas@is@plane}, def dPoints/.style 2 args={% @def dPoints/.code args={##1/##2/##3/##4}{% \path [#1] (##1,##2,##3) coordinate (##4); \tdcoord (##4) at (##1,##2,##3);}, @def dPoints/.list={#2} } } \makeatother

% save 3d coordinates of point \def\tdcoord #1 at (#2,#3,#4);{\pgfkeys{/tdcoords/#1/.is family,/tdcoords/#1,x/.initial=#2,y/.initial=#3,z/.initial=#4}}

% Get the 3d coordinate components of a point \def\GetX(#1){\pgfkeysvalueof{/tdcoords/(#1)/x}} \def\GetY(#1){\pgfkeysvalueof{/tdcoords/(#1)/y}} \def\GetZ(#1){\pgfkeysvalueof{/tdcoords/(#1)/z}}

% Define multiple 3d points following tkz-euclide notation \newcommand{\tkzDefdPoints}[2][]{\tikzset{def dPoints={#1}{#2}}}

\tdplotsetmaincoords{70}{110}

\begin{document} \begin{tikzpicture}[tdplot_main_coords] \draw[->] (0,0,0) -- (5,0,0) node[right]{$x$}; \draw[->] (0,0,0) -- (0,5,0) node[above]{$y$}; \draw[->] (0,0,0) -- (0,0,5) node[below left]{$z$};

\tkzDefdPoints{2/2/2/A,2/3/2/B,2/2/3/C, 2/2/2/D,2/3/2/E,2/2/3/F}

\tdcoord (A) at (2,2,2); \tdcoord (B) at (2,3,2); \tdcoord (C) at (2,2,3);

\draw (A) -- (B) -- (C) -- cycle;

\begin{scope}[canvasP={A,B,C}] \draw[red] (1,0) -- (1,1) -- (0,1); \end{scope}

\begin{scope}[canvasP={D,E,F}] \draw[blue] (0,1) -- (1,2) -- (2,1) -- (1,0); \end{scope} \end{tikzpicture} \end{document}

Qrrbrbirlbel
  • 119,821
  • Since \foreach parses list items starting with ( different, we could define @def dPoints so that we can use \tkzDefdPoints{(2,2,2)/A, (2,3,2)/B, (2,2,3)/C, (2,2,2)/D,(2,3,2)/E, (2,2,3)/F}. It probably is still a good idea to .expanded all values for x, y and z anyway. On another note: Nodes and coordinates are declared global by PGF/TikZ, so that one an reference a coordinate defined inside a scope (or even another picture). Unless you want to use \tkzDefdPoints inside a group (or a more complex \foreach loop) I wouldn't bother with it, though. – Qrrbrbirlbel Aug 28 '22 at 16:52
  • 1
    Having said that, I'd approach this problem very different. PGFkeys might not be the optimal tool to save these coordinates (after all, PGF doesn't use it either). – Qrrbrbirlbel Aug 28 '22 at 16:58
  • Thanks alot for your answer! Indeed solved both of my problems. It will take me a while to understand everything you dropped completely.

    Two things: I would be interested in a pointer on how you would solve it differently. And since you also answered my other question, would you drop the short form of your answer there too? I can do it as well, but wouldn't want to take your credit away since your answer here is the complete solution.

    – Shakaja Aug 28 '22 at 20:02