[Tex/LaTex] Custom shapes in TikZ

pgf-coretikz-pgf

I am trying to define a new shape in TikZ following the instructions of section 75.5 of the manual. Reading the question Drawing mechanical systems in LaTeX, I started to think that a "mechanical" library is really missing in TikZ, and it shouldn't, because it's useful just as libraries for circuits or diagrams. So I started in reproducing the symbol of a mechanical constraint:

enter image description here

(forget the vertical dashed line in the center) with the aim to recreate a set of symbols useful to draw mechanical systems and structures. That is what I've done:

\documentclass[a4paper]{article}

\usepackage{pgfplots}
\makeatletter
\pgfdeclareshape{carr}{%
  \inheritsavedanchors[from=circle]
  \inheritanchor[from=circle]{center}
  \inheritanchor[from=circle]{north}
  \inheritanchor[from=circle]{south}
  \inheritanchor[from=circle]{east}
  \inheritanchor[from=circle]{west}
  \backgroundpath{%
    % center
    \centerpoint \pgf@xa=\pgf@x   \pgf@ya=\pgf@y
    % west triangle corner
    \radius      \pgf@yb=-5\pgf@y \centerpoint \advance\pgf@yb by\pgf@y
      \pgf@xb=-2\pgf@x
    % east triangle corner
    \radius      \pgf@yc=-5\pgf@y \centerpoint \advance\pgf@yc by\pgf@y
      \pgf@xc=4\pgf@x
    % draw triangle..
    \pgfpathmoveto{\pgfpoint{\pgf@xa}{\pgf@ya}}%
    \pgfpathlineto{\pgfpoint{\pgf@xb}{\pgf@yb}}%
    \pgfpathlineto{\pgfpoint{\pgf@xc}{\pgf@yc}}%
    \pgfpathclose
    % central circle (from pgflibraryshapes.geometric.code.tex)..
    \pgf@process{\radius}
    \pgfutil@tempdima=\pgf@x%
    \pgfutil@tempdimb=\pgf@y%
    \pgfmathsetlength{\pgf@xb}{\pgfkeysvalueof{/pgf/outer xsep}}%  
    \pgfmathsetlength{\pgf@yb}{\pgfkeysvalueof{/pgf/outer ysep}}%  
    \advance\pgfutil@tempdima by-\pgf@xb%
    \advance\pgfutil@tempdimb by-\pgf@yb%
    \pgfpathellipse{\centerpoint}{\pgfqpoint{\pgfutil@tempdimb}{0pt}}{\pgfqpoint{0pt}{\pgfutil@tempdimb}}%
    % and lower circles
    \pgf@xa=1pt % the radius:should be a parameter
    \pgfutil@tempdima=\pgf@xb \advance\pgfutil@tempdima by\pgf@xa
    \pgfutil@tempdimb=\pgf@yb \advance\pgfutil@tempdimb by-\pgf@xa
    \pgfpathcircle{\pgfpoint{\pgfutil@tempdima}{\pgfutil@tempdimb}}{\pgf@xa}
    \pgfutil@tempdima=\pgf@xb \advance\pgfutil@tempdima by-\pgf@xa
    \pgfutil@tempdimb=\pgf@yb  \advance\pgfutil@tempdimb by-\pgf@xa
    \pgfpathcircle{\pgfpoint{\pgfutil@tempdima}{\pgfutil@tempdimb}}{\pgf@xa}
}}
\makeatother

\begin{document}
\begin{tikzpicture}
  \node[shape=carr,draw] at (3,0) {a};
  \node[shape=carr,draw] at (5,0) {}; % missing triangle
  \draw[shape=carr] (7,0); % nothing shown
\end{tikzpicture}
\end{document}

Looking at the code of other libraries, this seems to be the right way to procede, but correct me if I'm wrong. As you can see from the resulting pdf, the two lower circles in the figures are misplaced a lot, and I don't understand why. The code used to draw the upper circle is taken from the definition of the ellipse in tikz.
In addition, I don't know why, if I use an empty label in the node, the rectangle is missing. The last line of code (\draw[shape=carr....) is just an experiment. I would like to be able to write such a thing and see in the pdf just the reproduced shape, but I don't know if this is possible.

How to change the code to achieve the desider result? Do you think that this is just a waste of time? Suggestions on any direction are welcomed

Best Answer

Declaring shapes is one of the most useful features of the tikz/pgf library. It can be extended by endless choice and really is a good idea.

The problem with the shapes is the difficulties one has to go through to get it to work. This is due to the low-level coding that is necessary and essentially a good bookkeeping of variables is the problem. If one is not used to low-level variable reassignment this can become a cumbersome and even a task which cannot be completed without coding more than the basic layer offers.

This is also the problem of your code.

Commenting your code

\pgfdeclareshape{carr}{%
  \inheritsavedanchors[from=circle]
  \inheritanchor[from=circle]{center}
  \inheritanchor[from=circle]{north}
  \inheritanchor[from=circle]{south}
  \inheritanchor[from=circle]{east}
  \inheritanchor[from=circle]{west}

All this is fine, just keep it, you may realise that your east, west, etc. is not correctly placed. Thus you should add your own.

  \backgroundpath{%
    % center
    \centerpoint 
    \pgf@xa=\pgf@x   
    \pgf@ya=\pgf@y

This is also good, you acquire the center coordinates. However, the center coordinates only have a value if there is text in the node. They are defined as:
\pgf@x=.5\wd\pgfnodeparttextbox
\pgf@y=.5\ht\pgfnodeparttextbox
\advance\pgf@y by-.5\dp\pgfnodeparttextbox

Thus if no text is present they are both 0pt. This tells you why it didn't show if no node text is supplied.

    % west triangle corner
    \radius      
    \pgf@yb=-5\pgf@y 
    \centerpoint 
    \advance\pgf@yb by\pgf@y
    \pgf@xb=-2\pgf@x

Your problem here is that \radius is actually a dimension. You do not save this value in any way. You need to use \radius as a regular dimension:
\pgf@x=\radius

This means that what ever you are saving to \pgf@yb you have no idea of what is (or actually it is \pgf@y from \centerpoint, but that is another matter).

    % east triangle corner
    \radius
    \pgf@yc=-5\pgf@y
    \centerpoint
    \advance\pgf@yc by\pgf@y
    \pgf@xc=4\pgf@x

same problem here. This will not work.

    % draw triangle..
    \pgfpathmoveto{\pgfpoint{\pgf@xa}{\pgf@ya}}%
    \pgfpathlineto{\pgfpoint{\pgf@xb}{\pgf@yb}}%
    \pgfpathlineto{\pgfpoint{\pgf@xc}{\pgf@yc}}%
    \pgfpathclose

This is fine

    % central circle (from pgflibraryshapes.geometric.code.tex)..
    \pgf@process{\radius}

Here is another mistake. Let's go through what \pgf@process does.

Lets consider the following function:

\def\pgfcalc{\pgf@xb=2pt\pgf@x=\pgf@xb}

\pgfcalc will actually define both \pgf@xb and \pgf@x. In many cases you are only interested in the final result, which is \pgf@x and \pgf@y. In such case one will invoke \pgfcalc by \pgf@process.

\pgf@xb=1pt
\pgfcalc
% Here \pgf@xb and \pgf@x are set by \pgfcalc
% So \pgf@xb and \pgf@x are both 2pt
\pgf@xb=1pt
\pgf@process{\pgfcalc}
% Here ONLY \pgf@x is set. \pgf@xb is still 1pt

This means that you can only call \pgf@process on a macro and not a dimension (as \radius is). The code you refer to have \radius defined as an anchor which is a macro. You could have done it on \centerpoint if \centerpoint were to change any other than \pgf@x and \pgf@y.

For further information on the purposes see the manual.

Lets move on...

    \pgfutil@tempdima=\pgf@x%
    \pgfutil@tempdimb=\pgf@y%
    \pgfmathsetlength{\pgf@xb}{\pgfkeysvalueof{/pgf/outer xsep}}%  
    \pgfmathsetlength{\pgf@yb}{\pgfkeysvalueof{/pgf/outer ysep}}%  
    \advance\pgfutil@tempdima by-\pgf@xb%
    \advance\pgfutil@tempdimb by-\pgf@yb%
    \pgfpathellipse{\centerpoint}{\pgfqpoint{\pgfutil@tempdimb}{0pt}}{\pgfqpoint{0pt}{\pgfutil@tempdimb}}%
    % and lower circles
    \pgf@xa=1pt % the radius:should be a parameter
    \pgfutil@tempdima=\pgf@xb \advance\pgfutil@tempdima by\pgf@xa
    \pgfutil@tempdimb=\pgf@yb \advance\pgfutil@tempdimb by-\pgf@xa
    \pgfpathcircle{\pgfpoint{\pgfutil@tempdima}{\pgfutil@tempdimb}}{\pgf@xa}
    \pgfutil@tempdima=\pgf@xb \advance\pgfutil@tempdima by-\pgf@xa
    \pgfutil@tempdimb=\pgf@yb  \advance\pgfutil@tempdimb by-\pgf@xa
    \pgfpathcircle{\pgfpoint{\pgfutil@tempdima}{\pgfutil@tempdimb}}{\pgf@xa}
}}

This final part is actually ok, however, it is cumbersome to read and one can easily make mistakes. I would partition it a bit more.

A fast solution

This solution has been made so that you can work more with it. There are many options that you need to fine-tune and post-process.

It is up to you to complete with the top circle etc.

\documentclass{article}

\usepackage{tikz}
\makeatletter
\pgfdeclareshape{carr}{%
  \inheritsavedanchors[from=circle]
  \inheritanchor[from=circle]{center}
  \inheritanchor[from=circle]{north}
  \inheritanchor[from=circle]{south}
  \inheritanchor[from=circle]{east}
  \inheritanchor[from=circle]{west}
  \backgroundpath{%
      % Save radius to x
      \pgf@x=\radius
      % Radius is also containing the "minimum width" and "minimum height"
      % This ensures that even with no text the shape will be drawn.
      % Unless of course that min are set to 0pt
      % So no need to check for that
      % Save radius
      \pgfutil@tempdima=\pgf@x%

      % west triangle corner "b"
      \pgf@xb=-3\pgf@x%
      \pgf@yb=-4\pgf@x%
      % east triangle corner "c"
      \pgf@xc= 3\pgf@x%
      \pgf@yc=-4\pgf@x%

      % If text is present shift shape to center 
      % You need to shift more, but to get the idea
      \centerpoint
      \advance\pgf@xb by\pgf@x
      \advance\pgf@yb by\pgf@y
      \advance\pgf@xc by\pgf@x
      \advance\pgf@yc by\pgf@y

      % Save centerpoint in "a" (top triangle point)
      \pgf@xa=\pgf@x 
      \pgf@ya=\pgf@y

      % Below are good for debugging purposes.
      %\message{^^JTop : \the\pgf@xa,\the\pgf@ya}
      %\message{^^JWest: \the\pgf@xb,\the\pgf@yb}
      %\message{^^JEast: \the\pgf@xc,\the\pgf@yc}
      %\message{^^JCent: \the\pgf@x,\the\pgf@y}

      % draw triangle..
      \pgfpathmoveto{\pgfpoint{\pgf@xa}{\pgf@ya}}%
      \pgfpathlineto{\pgfpoint{\pgf@xb}{\pgf@yb}}%
      \pgfpathlineto{\pgfpoint{\pgf@xc}{\pgf@yc}}%
      \pgfpathclose

      % The radius of the small circles
      % Read in from option TODO
      \pgfutil@tempdimb=3pt

      % Move top triangle to head circle
      \advance\pgf@ya by.25\pgfutil@tempdimb
      % Move west triangle corner to west circle center
      \advance\pgf@xb by 1.5\pgfutil@tempdima
      \advance\pgf@yb by -\pgfutil@tempdimb
      % For handling line thickness if you wish "edge touch" and not "overlap"
      %\advance\pgf@yb by -.5\pgflinewidth 
      % Move east triangle corner to east circle center
      \advance\pgf@xc by-1.5\pgfutil@tempdima
      \advance\pgf@yc by -\pgfutil@tempdimb
      % For handling line thickness if you wish "edge touch" and not "overlap"
      %\advance\pgf@yc by -.5\pgflinewidth

      % This saves underlying "stuff" when you have the explicit `\pgfqpoint` and is thus a little faster
      \edef\pgf@marshal{%
          \noexpand\pgfpathcircle{%
              \noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}
          {\the\pgfutil@tempdimb}%
          \noexpand\pgfpathcircle{%
              \noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}
          {\the\pgfutil@tempdimb}%
          \noexpand\pgfpathcircle{%
              \noexpand\pgfqpoint{\the\pgf@xc}{\the\pgf@yc}}
          {\the\pgfutil@tempdimb}%
      }\pgf@marshal
  }}
\makeatother

\begin{document}
\begin{tikzpicture}
  \node[shape=carr,draw] at (3,0) {a};
  \node[shape=carr,draw] at (5,0) {}; % missing triangle (not anymore)
  % Your \draw example will never work! shapes are nodes, you need a node to assign the shape!
\end{tikzpicture}
\end{document}  

Here is the end result:

enter image description here

Good luck battling those shapes! :)