# Symbolic 3d coordinates in TikZ

Warning: answering this question may require some efforts. The purpose of question is to "teach TikZ 3d coordinates". What does that mean? If we define a coordinate in TikZ,

 \path (<x>,<y>) coordinate(A);


this coordinate A gets associated with 2 lengths that specify the location. In any transformed (shifted, rotated, slanted) coordinate system, we can still refer to this coordinate and, say, draw an arrow to it. What is more important for this question, we can always work backwards and figure out what the relative location to another coordinate is, e.g. with the calc library

\path let \p1=($(A)-(B)$),\n1={veclen(\x1,\y1)},\n2={atan2(\y1,\x1)} in <do something with this information>;


This is impossible in 3d since TikZ truncates the coordinates.

One possible way to deal with this has been proposed in this nice answer. This great, but does not quite work as smoothly as the above-mentioned calc syntax. Perhaps more importantly, one has to make extra efforts to store the 3d coordinates. Ideally, one would have something like

 \path (x,y,z) coordinate(A);


and TikZ would remember the z coordinate as well.

Notice that this request may sound more innocent at first glance than it really is. In 2d, we have a predefined reference frame, the screen coordinates. Moreover, the rotations form an Abelian group, so it is less cumbersome to keep track of them and to invert them. The above-mentioned answer saves the coordinates in local frames, so it is impossible to compare coordinates in different frames. However, this would be instrumental for many applications, in which one switches into, say, canvas is xy plane at z=0. Ideally, an answer to this question should associate each symbolic point with some three lengths that are the coordinates in a cleverly chosen reference frame, and there should be means to determine the relative location of two points in a coordinate-independent way, similarly to veclen in 2d.

In the best of all worlds, an answer would also come with an appropriate parser that allows us to do scalar products, vector products, compute the norm of a vector and do matrix multiplications, i.e. orthogonal transformations. (I think that going beyond orthogonal transformations is a mess because then matrix inversion will be really cumbersome.) Some progress regarding parsing has been made in the answers of this question but again it is probably fair to say that this not yet as convenient as the 2d counterparts.

Answers may or may not be based on tikz-3dplot. (tikz-3dplot comes with nice orthonormal projections.) Of course, the best of all options would be something that also works with the Three point perspective library.

Note that some matrix operations have been implemented in the calculator package. It is an impressive package that many things, and its routines may be useful for the task here. Whether or not there exist other packages of this sort, I do not know.

• Been a long time since I thought about 3d coordinates in TikZ so I'm not aware of the current state of play, ages ago I proposed a system for working with them in tex.stackexchange.com/a/52627/86 I'm not sure how close it gets to what you're proposing. – Andrew Stacey Aug 10 '19 at 8:48
• So, in summary, you want TikZ to be able to (1) store all coordinates when a point is specified in 3D, (2) use these for computations and (3) apply affine transformations (or just Euclidean transformations) to 3D points? That sounds like it'd be very useful, and a lot of work indeed… – Circumscribe Aug 11 '19 at 8:14
• I don't really understand why choosing a reference frame might be tricky or how the fact that rotations in 3D do not commute would a problem, however. The sensible way to keep track of transformations would probably be by storing a 3×3 transformation matrix and 3D vector for translations and to then perform the appropriate matrix multiplications whenever another transformation is applied. That's also how 2D transformations currently work (but the matrix is 2×2-and the vector has just 2 components). – Circumscribe Aug 11 '19 at 8:18
• @Circumscribe Yes, exactly. Ideally there are already matrix multiplications implemented somewhere. Orthogonal should be sufficient because inverting the others with LaTeX may be too messy. One way to rephrase the question: imagine we draw two points in two different frames (e.g. \tdplotsetrotatedcoords) and I want to ask TikZ what the 3d distance (NOT the distance on the screen!) is, and differences between coordinate values in either of the frames (or another frame). – user121799 Aug 11 '19 at 9:28
• I plan to integrate tikz-3dplot into PGF/TikZ at some point in the future (no ETA) with a better interface. However, I would have to contact the author of tikz-3dplot first and ask for permission. – Henri Menke Aug 13 '19 at 0:19

## 1 Answer

It is possible to cook up something along those lines. These are some results in that direction.

## Main point

One can hack TikZ to record the vielbein. Assuming that the user has an orthographic view, two basis vectors are sufficient. These two basis vectors have the components e_1=(\pgf@xx,\pgf@yx,\pgf@zx) and e_2=(\pgf@xy,\pgf@yy,\pgf@zy), the normal to the screen is simply e_3=e_1 x e_2. The (virtual) distance of a coordinate from the screen is called "screen depth" from now on. It simply is p.e_3, where p is a point.

In order to record the vielbein automatically, one needs to "hack" TikZ (or define a style for that). So, if you do not feel comfortable doing either of this, stop reading.

## Limitations

As of now, this works only for coordinates/nodes created in Cartesian coordinates, and scale factors are not (yet?) taken into account. Also, it might be desirable to have a syntax

\path let \p1=(A) in <do something with \z1>;


where \z1 is the screen depth. This is not (yet?) implemented.

## Explicit example

This code defines a function screendepth that returns the above-mentioned screen depth. Clearly, it is independent of the coordinate system. In particular, if one wants to achieve 3d ordering, objects with larger screen depths have to be drawn last. It works regardless of how you install the 3d view. For instance, we could have used tikz-3dplot instead of the perspective library.

\documentclass[tikz,border=3mm]{standalone}
\usetikzlibrary{calc,perspective}
\makeatletter
\pgfmathdeclarefunction{tdnormal}{6}{\begingroup
\pgfmathsetmacro\pgfutil@tmpa{(#2/1cm)*(#6)-(#3/1cm)*(#5)}%
\pgfmathsetmacro\pgfutil@tmpb{(#3/1cm)*(#4)-(#1/1cm)*(#6)}%
\pgfmathsetmacro\pgfutil@tmpc{(#1/1cm)*(#5)-(#2/1cm)*(#4)}%
\edef\pgfmathresult{\pgfutil@tmpa,\pgfutil@tmpb,\pgfutil@tmpc}%
\pgfmathsmuggle\pgfmathresult%
\endgroup}%
\pgfmathdeclarefunction{screendepth}{1}{\begingroup
\def\tikz@td@pp(##1){\edef\pgfutil@tmp{\csname tikz@dcl@coord@##1\endcsname}}%
\edef\pgfutil@tmp{\csname tikz@dcl@coord@#1\endcsname}%
\loop
\pgfutil@tempcnta=0%
\pgfutil@for\pgf@tmp:={\pgfutil@tmp}\do{\advance\pgfutil@tempcnta by1}%
\ifnum\pgfutil@tempcnta=1\relax
\expandafter\tikz@td@pp\pgfutil@tmp%
\repeat
\edef\pgfmathresult{0}%
\ifcase\pgfutil@tempcnta
\message{Something is wrong here.^^J}
\or
\message{Something is wrong here.^^J}
\or
\or
\edef\tikz@td@vielbein{\csname tikz@vielbein@#1\endcsname}%
\pgfmathsetmacro{\tikz@td@normal}{tdnormal(\tikz@td@vielbein)}%
\def\tikz@td@strip@brackets(##1,##2,##3)##4,##5,##6;{%
\edef\pgf@tmp{(##1)*(##4)+(##2)*(##5)+(##3)*(##6)}}%
\edef\temp{\noexpand\tikz@td@strip@brackets\pgfutil@tmp\tikz@td@normal;}%
\temp
\pgfmathparse{\pgf@tmp}%
\fi
\pgfmathsmuggle\pgfmathresult%
\endgroup}
\def\tikz@@fig@main{%
\pgfutil@ifundefined{pgf@sh@s@\tikz@shape}%
{\tikzerror{Unknown shape \tikz@shape.'' Using rectangle'' instead}%
\def\tikz@shape{rectangle}}%
{}%
\expandafter\xdef\csname tikz@dcl@coord@\tikz@fig@name\endcsname{%
\csname tikz@scan@point@coordinate\endcsname}%
\expandafter\xdef\csname tikz@vielbein@\tikz@fig@name\endcsname{%
\the\pgf@xx,\the\pgf@xy,\the\pgf@yx,\the\pgf@yy,\the\pgf@zx,\the\pgf@zy}%
\expandafter\xdef\csname tikz@trafo@\tikz@fig@name\endcsname{%
{{\pgf@pt@aa,\pgf@pt@ab},{\pgf@pt@ba,\pgf@pt@bb},%
{\the\pgf@pt@x,\the\pgf@pt@y}}}%
\tikzset{every \tikz@shape\space node/.try}%
\tikz@node@textfont%
\tikz@node@begin@hook%
\iftikz@is@matrix%
\let\tikz@next=\tikz@do@matrix%
\else%
\let\tikz@next=\tikz@do@fig%
\fi%
\tikz@next%
}%
\makeatother

\begin{document}
\begin{tikzpicture}[dot/.style={circle,fill,inner sep=1.2pt}]
\begin{scope}[3d view]
\draw[-stealth] (0,0,0) -- (2,0,0) node[pos=1.2]{$\vec x$};
\draw[-stealth] (0,0,0) -- (0,2,0) node[pos=1.2]{$\vec y$};
\draw[-stealth] (0,0,0) -- (0,0,2) node[pos=1.2]{$\vec z$};
\path[nodes=dot] (1,2,3) node (A){} (4,5) node (B){} (A) node (C){};
\path let \p1=(A),\p2=(B),\p3=(C) in
(A) node[above] {$A=({}$\x1,\y1,\pgfmathparse{screendepth("A")}\pgfmathresult pt)}
(B) node[above] {$B=({}$\x2,\y2,\pgfmathparse{screendepth("B")}\pgfmathresult pt)}
(C) node[below] {$C=({}$\x3,\y3,\pgfmathparse{screendepth("C")}\pgfmathresult pt)};
\end{scope}
\begin{scope}[xshift=6cm,3d view={110}{20}]
\draw[-stealth] (0,0,0) -- (2,0,0) node[pos=1.2]{$\vec x'$};
\draw[-stealth] (0,0,0) -- (0,2,0) node[pos=1.2]{$\vec y'$};
\draw[-stealth] (0,0,0) -- (0,0,2) node[pos=1.2]{$\vec z'$};
\path[nodes=dot] (1,2,3) node (A'){} (4,5) node (B'){} (A') node (C'){};
\path let \p1=(A'),\p2=(B'),\p3=(C') in
(A') node[above] {$A'=({}$\x1,\y1,\pgfmathparse{screendepth("A'")}\pgfmathresult pt)}
(B') node[above] {$B'=({}$\x2,\y2,\pgfmathparse{screendepth("B'")}\pgfmathresult pt)}
(C') node[below] {$C'=({}$\x3,\y3,\pgfmathparse{screendepth("C'")}\pgfmathresult pt)};
\end{scope}
\end{tikzpicture}
\end{document}


The result is not catchy or anything but an attempt to make 3d ordering in TikZ a bit less cumbersome.

Alternatively one can "hack" calc instead of TikZ. This hack is not completely symmetric, one has to refer to the coordinate by its original name, and of course one cannot use something like ($(A)+(B)$). Doing this would require a more substantial surgery. However, you can get the "physical" components with the calc syntax.

\documentclass[tikz,border=3mm]{standalone}
\usetikzlibrary{calc,perspective}
\makeatletter
\pgfmathdeclarefunction{tdnormal}{6}{\begingroup
\pgfmathsetmacro\pgfutil@tmpa{(#2/1cm)*(#6)-(#3/1cm)*(#5)}%
\pgfmathsetmacro\pgfutil@tmpb{(#3/1cm)*(#4)-(#1/1cm)*(#6)}%
\pgfmathsetmacro\pgfutil@tmpc{(#1/1cm)*(#5)-(#2/1cm)*(#4)}%
\edef\pgfmathresult{\pgfutil@tmpa,\pgfutil@tmpb,\pgfutil@tmpc}%
\pgfmathsmuggle\pgfmathresult%
\endgroup}%
\pgfmathdeclarefunction{z3d}{1}{\begingroup
\def\tikz@td@pp(##1){\edef\pgfutil@tmp{\csname tikz@dcl@coord@##1\endcsname}}%
\edef\pgfutil@tmp{\csname tikz@dcl@coord@#1\endcsname}%
\loop
\pgfutil@tempcnta=0%
\pgfutil@for\pgf@tmp:={\pgfutil@tmp}\do{\advance\pgfutil@tempcnta by1}%
\ifnum\pgfutil@tempcnta=1\relax
\expandafter\tikz@td@pp\pgfutil@tmp%
\repeat
\edef\pgfmathresult{0}%
\ifcase\pgfutil@tempcnta
\message{Something is wrong here.^^J}%
\or
\message{Something is wrong here.^^J}%
\or
\or
\pgfmathsetmacro{\tikz@td@normal}{tdnormal(\the\pgf@xx,\the\pgf@xy,\the\pgf@yx,\the\pgf@yy,\the\pgf@zx,\the\pgf@zy)}%
\def\tikz@td@strip@brackets(##1,##2,##3)##4,##5,##6;{%
\edef\pgf@tmp{(##1)*(##4)+(##2)*(##5)+(##3)*(##6)}}%
\edef\temp{\noexpand\tikz@td@strip@brackets\pgfutil@tmp\tikz@td@normal;}%
\temp
\pgfmathparse{\pgf@tmp}%
\fi
\pgfmathsmuggle\pgfmathresult%
\endgroup}
\def\tikz@let@command et{%
\let\p=\tikz@cc@dop%
\let\x=\tikz@cc@dox%
\let\y=\tikz@cc@doy%
\let\z=\tikz@cc@doz%
\let\n=\tikz@cc@don%
\pgfutil@ifnextchar i{\tikz@cc@stop@let}{\tikz@cc@handle@line}%
}%
\def\tikz@cc@doz#1{\csname tikz@cc@z@#1\endcsname}%
\def\tikz@cc@dolet#1{%
\pgf@process{#1}%
\expandafter\edef\csname tikz@cc@p@\tikz@cc@coord@name\endcsname{\the\pgf@x,\the\pgf@y}%
\expandafter\edef\csname tikz@cc@x@\tikz@cc@coord@name\endcsname{\the\pgf@x}%
\expandafter\edef\csname tikz@cc@y@\tikz@cc@coord@name\endcsname{\the\pgf@y}%
\pgfutil@ifnextchar,{\tikz@cc@handle@nextline}{\tikz@cc@stop@let}%
}%
\tikzset{record z/.style={execute at end node={%
\pgfmathparse{z3d("\tikz@fig@name")}%
\expandafter\xdef\csname tikz@cc@z@\tikz@fig@name\endcsname{\pgfmathresult pt}}}}
\makeatother

\begin{document}
\begin{tikzpicture}[dot/.style={circle,fill,inner sep=1.2pt,record z}]
\begin{scope}[3d view]
\draw[-stealth] (0,0,0) -- (2,0,0) node[pos=1.2]{$\vec x$};
\draw[-stealth] (0,0,0) -- (0,2,0) node[pos=1.2]{$\vec y$};
\draw[-stealth] (0,0,0) -- (0,0,2) node[pos=1.2]{$\vec z$};
\path[nodes=dot] (1,2,3) node (A){} (4,5) node (B){} (A) node (C){};
\path let \p1=(A),\p2=(B),\p3=(C) in
(A) node[above] {$A=({}$\x1,\y1,\z{A})}
(B) node[above] {$B=({}$\x2,\y2,\z{B}\pgfmathresult pt)}
(C) node[below] {$C=({}$\x3,\y3,\z{C})};
\end{scope}
\begin{scope}[xshift=6cm,3d view={110}{20}]
\draw[-stealth] (0,0,0) -- (2,0,0) node[pos=1.2]{$\vec x'$};
\draw[-stealth] (0,0,0) -- (0,2,0) node[pos=1.2]{$\vec y'$};
\draw[-stealth] (0,0,0) -- (0,0,2) node[pos=1.2]{$\vec z'$};
\path[nodes=dot] (1,2,3) node (A'){} (4,5) node (B'){} (A') node (C'){};
\path let \p1=(A'),\p2=(B'),\p3=(C'),\p4=(A),\p5=(B),\p6=(C) in
(A') node[above] {$A'=({}$\x1,\y1,\z{A'})}
(B') node[above] {$B'=({}$\x2,\y2,\z{B'})}
(C') node[below] {$C'=({}$\x3,\y3,\z{C'})}
(A) edge[edge label={\pgfmathparse{sqrt(pow(\x1/1cm-\x4/1cm,2)+pow(\y1/1cm-\y4/1cm,2)+pow(\z{A}/1cm-\z{A'}/1cm,2))}%
$d=\pgfmathprintnumber\pgfmathresult$cm}] (A');
\end{scope}
\end{tikzpicture}
\end{document}


Note that the z3d function can be used regardless of possible hacks, however, it computes the z component assuming that the user did not switch their coordinate system.