[Tex/LaTex] tikz and \pgfdeclareshape why the text is not at the center anchor

tikz-pgf

Why is the text (the black bullets) not at coordinate (0,0) where I have defined the anchor center but is a bit above on the right ?

\documentclass[border=10pt, tikz]{standalone}
\usepackage{tikz}
\usetikzlibrary{calc,shapes.geometric,shapes.symbols}

\tikzset{%
couleur/.style={fill={#1},top couleur/.style={fill=#1!50}},%
}%

\makeatletter%

\pgfkeys{/tikz/remplissage/.initial = -1cm}%hauteur de fluide -1 = vide

% ---------------- %
% erlenmeyer %
% ---------------- %
\pgfdeclareshape{erlenmeyer}{ \nodeparts{text}
\anchor{center}{\pgfpoint{0}{0cm}}%
\anchor{north}{\pgfpoint{0}{1.85cm}}%
\anchor{south}{\pgfpoint{0}{-2cm}}%
\saveddimen\hauteurphase{\pgf@x=\pgfkeysvalueof{/tikz/remplissage}}%

\behindbackgroundpath{ %
\path[draw,clip] (-0.5,1.75) to[rounded
corners=2pt]++(0,-1)to[rounded corners=10pt]++(-1,-2.5)to[rounded
corners=10pt, bend right=15pt]++(3,0) to[rounded
corners=2pt]++(-1,2.5)--++(0,1)++(-0.5,0) circle [x radius=0.5, y
radius=0.1];%
\path[couleur](-1.6,-2) rectangle (1.6,{\hauteurphase-2cm});
\path[draw=white,top couleur] (0,\hauteurphase-2cm) circle [x
radius=1.5cm-(\hauteurphase-0.25cm)*0.4 , y
radius=0.1 cm];%
}%
}

\makeatother

\begin{document}

\begin{tikzpicture}[couleur = green, remplissage = 1cm]

  \draw (0,0)node[erlenmeyer]{$\bullet$};%
  \draw [help lines] (0,-2) node{(0,-2)}grid(3,2)node{(3,2)};%

\end{tikzpicture}


\end{document}

enter image description here


@cfr Indeed dealing with \anchor{text} and \pgfnodeparttextbox solved that problem but introduce an other one that I will talk later.

I read and tried to understand the manual p1035 as you suggested
I think that the code has to specify the coordinate of the lower left text box corner in the shape coordinate system.
To put the text centered in the origin, the lower left text box corner has to be placed half of width on the left of the origin and half of height below the origin.

\documentclass[border=10pt, tikz]{standalone}
\usepackage{tikz}
\usetikzlibrary{calc,shapes.geometric,shapes.symbols}



\tikzset{%
 couleur/.style={fill={#1},top couleur/.style={fill=#1!50}},%
}%

\makeatletter%

\pgfkeys{/tikz/remplissage/.initial = -1
 cm}%hauteur de fluide -1 = vide




% ---------------- %
% erlenmeyer %
% ---------------- %
\pgfdeclareshape{erlenmeyer}{%
  \nodeparts{text}%
  \savedanchor{\lowerlefttextcorner}{
\pgf@y=-0.5\ht\pgfnodeparttextbox % height of the box, ignoring the depth
\pgf@x=-0.5\wd\pgfnodeparttextbox % width of the box
 }%
\anchor{center}{\pgfpoint{0}{0cm}}%
\anchor{north}{\pgfpoint{0}{1.85cm}}%
\anchor{south}{\pgfpoint{0}{-2cm}}%

\anchor{text}{%
\lowerlefttextcorner%
}%

\saveddimen\hauteurphase{\pgf@x=\pgfkeysvalueof{/tikz/remplissage}}%

\behindbackgroundpath{ %
\path[draw,clip] (-0.5,1.75) to[rounded
corners=2pt]++(0,-1)to[rounded corners=10pt]++(-1,-2.5)to[rounded
corners=10pt, bend right=15pt]++(3,0) to[rounded
corners=2pt]++(-1,2.5)--++(0,1)++(-0.5,0) circle [x radius=0.5, y
radius=0.1];%
\path[couleur](-1.6,-2) rectangle (1.6,{\hauteurphase-2cm});
\path[draw=white,top couleur] (0,\hauteurphase-2cm) circle [x
radius=1.5cm-(\hauteurphase-0.25cm)*0.4 , y
radius=0.1 cm];%
}%
  }





\makeatother

\begin{document}

\begin{tikzpicture}[anchor=center, remplissage = 1cm]
  \draw[olive, ultra thick] (0,0)node[scale=2]{$\bullet$} --(3,0) node[scale=2]{$\bullet$};%

  \draw (0,0)node[erlenmeyer, couleur = cyan]{$\bullet$}
  ++ (3,0)node[erlenmeyer, couleur = yellow]{$\bullet$};%

  \draw [help lines] (0,-2) node[olive]{this text is well
placed}grid(3,2)node[olive]{this text is well placed};%
\draw (0,-3)node[right]{With anchor center};
\end{tikzpicture}

\begin{tikzpicture}[anchor=south, remplissage = 1cm]
  \draw[red, ultra thick] (0,0)node[scale=2]{$\bullet$} --(3,0) node[scale=2]{$\bullet$};%

  \draw (0,0)node[erlenmeyer, couleur = red]{$\bullet$}
  ++ (3,0)node[erlenmeyer, couleur = blue]{$\bullet$};%

  \draw [help lines] (0,-2) node[red]{this text is higher than I
expected}grid(3,2)node[red]{this text is higher than I expected};%
\draw (0,-3)node[right]{With anchor south: and where are the scaled twice bullets?};

\end{tikzpicture}

\begin{tikzpicture}[anchor=north, remplissage = 1cm]
  \draw[red, ultra thick] (0,0)node[scale=2]{$\bullet$} --(3,0)
  node[scale=2]{$\bullet$};%

  \draw (0,0)node[erlenmeyer, couleur = magenta]{$\bullet$}
  ++ (3,0)node[erlenmeyer, couleur = olive]{$\bullet$};%

  \draw [help lines] (0,-2) node[red]{this text is lower than I
expected}grid(3,2)node[red]{this text is lower than I expected};%
\draw (0,-5)node[right]{With anchor north: and why are the scaled
  twice bullets so low?};

\end{tikzpicture}




\end{document}

The erlenmeyer node with center anchor parameter is working but the new problem is that the south and north anchor parameters are working well for the erlenmeyer placement but have sides effects: they are destroying the placement of the other stuff outside of the erlenmeyer node.

enter image description here

As cfr said if I want north or south alignement, I have to pass it as a node option so it doesn't affect everything.

\begin{tikzpicture}[anchor=center, remplissage = 1cm]
  \draw[red, ultra thick] (0,0)node[scale=2]{$\bullet$} --(3,0) node[scale=2]{$\bullet$};%

  \draw (0,0)node[erlenmeyer, couleur = cyan]{$\bullet$}
  ++ (3,0)node[erlenmeyer, couleur = yellow]{$\bullet$};%

  \draw [help lines] (0,-2) node{(0,-2)}grid(3,2)node{(3,2)};%

\draw (0,-3)node[right]{With anchor center};
\end{tikzpicture}

\begin{tikzpicture}[ remplissage = 1cm]
  \draw[red, ultra thick] (0,0)node[scale=2]{$\bullet$} --(3,0)          node[scale=2]{$\bullet$};%

  \draw (0,0)node[erlenmeyer,anchor=south, couleur = red]{$\bullet$}
  ++ (3,0)node[erlenmeyer ,anchor=south, couleur = blue]{$\bullet$};%

  \draw [help lines] (0,-2) node{(0,-2)}grid(3,2)node{(3,2)};%

\draw (0,-3)node[right]{With anchor south};

\end{tikzpicture}

\begin{tikzpicture}[remplissage = 1cm]
  \draw[red, ultra thick] (0,0)node[scale=2]{$\bullet$} --(3,0)
  node[scale=2]{$\bullet$};%

  \draw (0,0)node[erlenmeyer, anchor=north, couleur = magenta]{$\bullet$}
  ++ (3,0)node[erlenmeyer, anchor=north, couleur = olive]{$\bullet$};%

 \draw [help lines] (0,-2) node{(0,-2)}grid(3,2)node{(3,2)};%
\draw (0,-5)node[right]{With anchor north};

\end{tikzpicture}

enter image description here

Best Answer

Note that I've never, ever played with \pgfdeclareshape or similar stuff before.

Caveat emptor...

From a quick skim of the relevant section of the manual, it seems to me that your code just doesn't do the work required to set up a bona fide node. That requires creating a suitable anchor for the text label and a suitable \savedanchor along with it. To make the anchors accessible normally in pictures, you then need to create regular anchors based on the \savedanchors. For example:

\documentclass[border=10pt,tikz,multi]{standalone}
\usetikzlibrary{calc,shapes.geometric,shapes.symbols,positioning}
\tikzset{%
  couleur/.style={fill={#1},top couleur/.style={fill=#1!50}},
  remplissage/.store in=\hauteurphase,
  remplissage=-1cm,
}
\makeatletter%
\pgfdeclareshape{erlenmeyer}{%
  \nodeparts{text}%
  \savedanchor{\mymacroname}{% manual 1035
    \pgf@y=.5\ht\pgfnodeparttextbox % height of the box
    \pgf@x=.5\wd\pgfnodeparttextbox % width of the box
    \setlength{\pgf@xa}{.5\pgfshapeminwidth}%
    \ifdim\pgf@x<.5\pgf@xa
      \pgf@x=\pgf@xa
    \fi
  }%
  \savedanchor{\topname}{% manual 1035
    \pgf@y=1.85cm
    \pgf@x=0pt
  }%
  \savedanchor{\downbelowname}{% manual 1035
    \pgf@y=-2cm
    \pgf@x=0pt
  }%
  \anchor{center}{\pgfpointorigin}%
  \anchor{north}{\topname}%
  \anchor{south}{\downbelowname}%
  \anchor{text}{%
    \mymacroname
    \pgf@x=-\pgf@x
    \pgf@y=-\pgf@y
  }%
  \savedanchor{\overthere}{%
    \pgf@y=0pt
    \pgf@x=1.6\hauteurphase
  }%
  \anchor{east}{\overthere}%
  \savedanchor{\overhere}{%
    \pgf@y=0pt
    \pgf@x=-1.6\hauteurphase
  }%
  \anchor{west}{\overhere}%
  \behindbackgroundpath{%
    \path [draw, clip] (-0.5,1.75) to [rounded corners=2pt] ++(0,-1) to [rounded corners=10pt] ++(-1,-2.5) to [rounded corners=10pt, bend right=15pt] ++(3,0) to [rounded corners=2pt] ++(-1,2.5)-- ++(0,1) ++(-0.5,0) circle [x radius=0.5, y radius=0.1];
    \path [couleur] (-1.6,-2) rectangle (1.6,{\hauteurphase-2cm});
    \path [draw=white, top couleur] (0,\hauteurphase-2cm) circle [x radius=1.5cm-(\hauteurphase-0.25cm)*0.4 , y radius=0.1 cm];
  }%
}
\makeatother

\begin{document}
\begin{tikzpicture}[couleur = green, remplissage = 1cm]
  \draw [help lines] (0,-2) node {(0,-2)} grid (10,2) node {(10,2)};
  \draw (0,0) node (a) [erlenmeyer] {$\bullet\bullet\bullet$};
  \node [right=5pt of a, erlenmeyer, couleur=magenta] {text};
\end{tikzpicture}
\end{document}

centred bullets etc.

This is not all that needs to be done. If we want to be able to place our nodes relative to, say, their northern anchors, then we need to ensure that the node's shape is appropriately sensitive to such requests. Right now, the node has no shape. Some stuff is drawn in the background but the node has no \backgroundpath to define its shape.

To define a shape for the node so that its anchors work as expected, we need to use low-level PGF commands to define the path.

Let's create some dimensions to hold positions for PGF points which we know won't get overwritten unexpectedly:

\newdimen\em@xa
\newdimen\em@ya
\newdimen\em@xb
\newdimen\em@yb
\newdimen\em@xc
\newdimen\em@yc

Then we start as before but add a few more anchors which are useful for placement:

\pgfdeclareshape{erlenmeyer}{%
  ...
  \savedanchor{\upright}{%
    \pgf@x=1.6\hauteurphase
    \pgf@y=1.85cm
  }%
  \anchor{north east}{\upright}%
  \anchor{south west}{%
    \upright
    \pgf@x=-\pgf@x
    \pgf@y=-\pgf@y
  }%
  \anchor{south east}{%
    \upright
    \pgf@x=\pgf@x
    \pgf@y=-\pgf@y
  }%
  \anchor{north west}{%
    \upright
    \pgf@x=-\pgf@x
    \pgf@y=\pgf@y
  }%

Now to define our node's shape:

  \backgroundpath{%

These are the dimensions we need:

    \em@xa=-5mm \em@ya=+17.5mm
    \em@xb=+15mm \em@yb=+7.5mm
    \downbelowname
    \em@xc=\pgf@x \em@yc=1.1\pgf@y

Move to the start of the path:

    \pgfpathmoveto{\pgfpoint{\em@xa}{\em@ya}}%

And now define the node's shape:

    \pgfsetcornersarced{\pgfpoint{2pt}{2pt}}%
    \pgfpathlineto{\pgfpoint{\em@xa}{\em@yb}}%
    \pgfsetcornersarced{\pgfpoint{10pt}{10pt}}%
    \pgfpathlineto{\pgfpoint{-\em@xb}{-\em@ya}}%
    \pgfpathquadraticcurveto{\pgfpoint{\em@xc}{\em@yc}}{\pgfpoint{\em@xb}{-\em@ya}}%
    \pgfsetcornersarced{\pgfpoint{2pt}{2pt}}%
    \pgfpathlineto{\pgfpoint{-\em@xa}{\em@yb}}%
    \pgfsetcornersarced{\pgfpointorigin}%
    \pgfpathlineto{\pgfpoint{-\em@xa}{\em@ya}}%
    \pgfpathmoveto{\pgfpoint{\em@xa}{\em@ya}}%
    \pgfpathellipse{\pgfpoint{0pt}{\em@ya}}{\pgfpoint{5mm}{0mm}}{\pgfpoint{0mm}{1mm}}%
  }%

Don't draw in the background as we can draw the node's shape if we wish and it will pick up appropriately on all the usual option goodies other nodes use. We change \behindbackgroundpath to \beforebackgroundpath so that if we fill the node, for example, it will be filled behind the flask's liquid contents as we'd expect:

  \beforebackgroundpath{%
    \path [clip] (-0.5,1.75) to [rounded corners=2pt] ++(0,-1) to [rounded corners=10pt] ++(-1,-2.5) to [rounded corners=10pt, bend right=15pt] ++(3,0) to [rounded corners=2pt] ++(-1,2.5)-- ++(0,1) ++(-0.5,0) circle [x radius=0.5, y radius=0.1];

This sets the fill colours for the liquid a little differently as we no longer want to pass e.g. fill=magenta to the whole node, which couleur=magenta did. More on this in a minute.

    \path [fill=erlenmeyer couleur] (-1.6,-2) rectangle (1.6,{\hauteurphase-2cm});
    \path [draw=white, fill=top couleur] (0,\hauteurphase-2cm) circle [x radius=1.5cm-(\hauteurphase-0.25cm)*0.4 , y radius=0.1 cm];
  }%
}

So how to allow filling behind the liquid? Here's one way which preserves the current couleur=<liquid colour> interface:

\tikzset{%

A couple of keys to execute code setting up the colours we used to fill the flask above:

  erlenmeyer couleur/.code={\colorlet{erlenmeyer couleur}{#1}},
  top couleur/.code={\colorlet{top couleur}{#1}},

And here's the style to set the liquid colour in pictures. This uses the keys above. We could equally have made this execute the \colorlet... commands but this is potentially more flexible.

  couleur/.style={%
    erlenmeyer couleur=#1,
    top couleur=#1!50,
  },

Make sure the colours are always defined to something:

  couleur=gray,

Now we can draw some nodes using code like this:

\begin{tikzpicture}[couleur = green, remplissage = 1cm]
  \draw [help lines] (-2,-4) grid (10,2);
  \node (a) [erlenmeyer, draw] at (-.25,0) {$\bullet\bullet\bullet$};
  \node (b) [right=5pt of a.east, erlenmeyer, anchor=north west, draw=magenta!50!black, top color=white, bottom color=orange!50!magenta, thick, couleur=magenta] {text};
  \node (c) [right=5pt of b.east, erlenmeyer, anchor=south west, draw=blue, double=cyan, couleur=blue!50!cyan] {$\bullet$};
  \node (d) [right=5pt of c.south east, erlenmeyer, anchor=south east, top color=white, bottom color=yellow, draw, couleur=orange, draw=red, scale=-.5] {*};
\end{tikzpicture}

to produce a result like this:

well-behaved nodes

These nodes now behave as they should i.e. as nodes usually behave for this kind of usage. We have not defined a border path so anchors such as .57 won't make sense. Nor have we defined additional anchors such as mid or base. Moreover, our node has fixed dimensions, which isn't usual. But we do have the standard compass point anchors defined, as well as center and draw, fill etc. keys influence the appearance of our nodes in the expected ways.

Note that all node shapes put their text boxes in standard places relative to their coordinate system. If you say anchor=north, that doesn't change the location of the text within the node shape. Rather, it changes the way in which the node is placed relative to the external coordinate system.

Compare this example:

\documentclass[border=10pt,tikz,multi]{standalone}
\usetikzlibrary{positioning,shapes.geometric}
\begin{document}
\begin{tikzpicture}
  \node (a) [isosceles triangle, draw] at (-.25,0) {$\bullet\bullet\bullet$};
  \node (b) [right=5pt of a.east, isosceles triangle, anchor=north west, draw=magenta!50!black, top color=white, bottom color=orange!50!magenta, thick] {text};
  \node (c) [right=5pt of b.east, isosceles triangle, anchor=south west, draw=blue, double=cyan] {$\bullet$};
  \node (d) [right=5pt of c.south east, isosceles triangle, anchor=south east, top color=white, bottom color=yellow, draw, draw=red, scale=-.5] {*};
\end{tikzpicture}
\end{document}

which produces

triangular nodes

The anchor choice does not change the position of the text within the shape isosceles triangle. It changes which anchor point within the shape is used to position the node. So text doesn't go to the top when anchor=north. Rather, text stays where it would be anyway within the isosceles triangle's shape coordinate system. What changes is that the shape's .north anchor is aligned with the specified location (5pt to the right of a.east) rather than the default .center one.

Complete code:

\documentclass[border=10pt,tikz,multi]{standalone}
\usetikzlibrary{positioning}
\tikzset{%
  erlenmeyer couleur/.code={\colorlet{erlenmeyer couleur}{#1}},
  top couleur/.code={\colorlet{top couleur}{#1}},
  couleur/.style={%
    erlenmeyer couleur=#1,
    top couleur=#1!50,
  },
  couleur=gray,
  remplissage/.store in=\hauteurphase,
  remplissage=-1cm,
}
\makeatletter%
\newdimen\em@xa
\newdimen\em@ya
\newdimen\em@xb
\newdimen\em@yb
\newdimen\em@xc
\newdimen\em@yc
\pgfdeclareshape{erlenmeyer}{%
  \nodeparts{text}%
  \savedanchor{\mymacroname}{% manual 1035
    \pgf@y=.5\ht\pgfnodeparttextbox % height of the box
    \pgf@x=.5\wd\pgfnodeparttextbox % width of the box
    \setlength{\pgf@xa}{.5\pgfshapeminwidth}%
    \ifdim\pgf@x<.5\pgf@xa
      \pgf@x=\pgf@xa
    \fi
  }%
  \savedanchor{\topname}{% manual 1035
    \pgf@y=1.85cm
    \pgf@x=0pt
  }%
  \savedanchor{\downbelowname}{% manual 1035
    \pgf@y=-2cm
    \pgf@x=0pt
  }%
  \anchor{center}{\pgfpointorigin}%
  \anchor{north}{\topname}%
  \anchor{south}{\downbelowname}%
  \anchor{text}{%
    \mymacroname
    \pgf@x=-\pgf@x
    \pgf@y=-\pgf@y
  }%
  \savedanchor{\overthere}{%
    \pgf@y=0pt
    \pgf@x=1.6\hauteurphase
  }%
  \anchor{east}{\overthere}%
  \savedanchor{\overhere}{%
    \pgf@y=0pt
    \pgf@x=-1.6\hauteurphase
  }%
  \anchor{west}{\overhere}%
  \savedanchor{\upright}{%
    \pgf@x=1.6\hauteurphase
    \pgf@y=1.85cm
  }%
  \anchor{north east}{\upright}%
  \anchor{south west}{%
    \upright
    \pgf@x=-\pgf@x
    \pgf@y=-\pgf@y
  }%
  \anchor{south east}{%
    \upright
    \pgf@x=\pgf@x
    \pgf@y=-\pgf@y
  }%
  \anchor{north west}{%
    \upright
    \pgf@x=-\pgf@x
    \pgf@y=\pgf@y
  }%
  \backgroundpath{%
    \em@xa=-5mm \em@ya=+17.5mm
    \em@xb=+15mm \em@yb=+7.5mm
    \downbelowname
    \em@xc=\pgf@x \em@yc=1.1\pgf@y
    \pgfpathmoveto{\pgfpoint{\em@xa}{\em@ya}}%
    \pgfsetcornersarced{\pgfpoint{2pt}{2pt}}%
    \pgfpathlineto{\pgfpoint{\em@xa}{\em@yb}}%
    \pgfsetcornersarced{\pgfpoint{10pt}{10pt}}%
    \pgfpathlineto{\pgfpoint{-\em@xb}{-\em@ya}}%
    \pgfpathquadraticcurveto{\pgfpoint{\em@xc}{\em@yc}}{\pgfpoint{\em@xb}{-\em@ya}}%
    \pgfsetcornersarced{\pgfpoint{2pt}{2pt}}%
    \pgfpathlineto{\pgfpoint{-\em@xa}{\em@yb}}%
    \pgfsetcornersarced{\pgfpointorigin}%
    \pgfpathlineto{\pgfpoint{-\em@xa}{\em@ya}}%
    \pgfpathmoveto{\pgfpoint{\em@xa}{\em@ya}}%
    \pgfpathellipse{\pgfpoint{0pt}{\em@ya}}{\pgfpoint{5mm}{0mm}}{\pgfpoint{0mm}{1mm}}%
  }%
  \beforebackgroundpath{%
    \path [clip] (-0.5,1.75) to [rounded corners=2pt] ++(0,-1) to [rounded corners=10pt] ++(-1,-2.5) to [rounded corners=10pt, bend right=15pt] ++(3,0) to [rounded corners=2pt] ++(-1,2.5)-- ++(0,1) ++(-0.5,0) circle [x radius=0.5, y radius=0.1];
    \path [fill=erlenmeyer couleur] (-1.6,-2) rectangle (1.6,{\hauteurphase-2cm});
    \path [draw=white, fill=top couleur] (0,\hauteurphase-2cm) circle [x radius=1.5cm-(\hauteurphase-0.25cm)*0.4 , y radius=0.1 cm];
  }%
}
\makeatother

\begin{document}
\begin{tikzpicture}[couleur = green, remplissage = 1cm]
  \draw [help lines] (-2,-4) grid (10,2);
  \node (a) [erlenmeyer, draw] at (-.25,0) {$\bullet\bullet\bullet$};
  \node (b) [right=5pt of a.east, erlenmeyer, anchor=north west, draw=magenta!50!black, top color=white, bottom color=orange!50!magenta, thick, couleur=magenta] {text};
  \node (c) [right=5pt of b.east, erlenmeyer, anchor=south west, draw=blue, double=cyan, couleur=blue!50!cyan] {$\bullet$};
  \node (d) [right=5pt of c.south east, erlenmeyer, anchor=south east, top color=white, bottom color=yellow, draw, couleur=orange, draw=red, scale=-.5] {*};
\end{tikzpicture}
\end{document}