4

The following MWE is a very abstracted version of some code in a library that I'm writing:

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{calc}
\begin{document}

\begin{tikzpicture}
    \node[draw] at (1,0) (A) {A};
    \coordinate (connection) at ($(A.north west)!0.66!(A.south west)$);
    \path[draw] (0,0) -- (connection);
\end{tikzpicture}

\end{document}

enter image description here

What I want to do is to move the 'A' node and the 'connection' coordinate upward a little bit, such that the line becomes perfectly horizontal.

Previously that was no problem, because I knew the node height in advance and I could just use the calc library to calculate the correct yshift, before drawing the node and the coordinate. However, now I'd like to do this in a way that depends on the height of the node, which might depend on its contents. So it seems like I have to draw the node first, before calculating its height and then moving it.

I know that I can get the height dynamically, as explained at How can I get the height of a TikZ node and store it in a length?, but I don't know if it's possible to move the node after it's been drawn.

Alternatively, is there a way to simply say "position the node's anchor at a point exactly 2/3 of the way from its north west to its south west?" If that can be done (for an arbitrary fraction) it would solve my problem.

6
  • 2
    To your alternative question: No but we can define anchors that would do that. There is something called Deferred Node Positioning (used by graphdrawing) but it might be a bit annoying to implement yourself. You could do something similar by first creating the node but discarding the output (used by forest). The node then exists to be measured and can then be created again with the appropriate positioning. Commented Jul 23, 2023 at 3:52
  • 1
    Maybe you just want to use the mid and mid west anchors? The asymmetrical recrangle shape by tikz-cd has the west, center and east anchors an absolute distance above the base line where the line will not point towards the true centerof the node. Commented Jul 23, 2023 at 3:57
  • @Qrrbrbirlbel I need it to be an exact fraction of the distance between the top left corner and the bottom right. The reason is that each node has n wires into it, where n depends on the node, and I want the ability to line up any one of those wires with another coordinate to make it horizontal.
    – N. Virgo
    Commented Jul 23, 2023 at 4:36
  • @Qrr oh, I didn't see your first comment. I'll look into those options, thank you
    – N. Virgo
    Commented Jul 23, 2023 at 4:52
  • 2
    Probably the ideal thing is to make the wire input and output ports anchors instead of coordinates. I will try looking into that as well. It seems to involve learning to use the pgf layer, though.
    – N. Virgo
    Commented Jul 23, 2023 at 4:54

3 Answers 3

5

Generic anchors provide an easy interface to add new anchors to (all) shapes. Though, in this case, they don't make much sense for circular nodes but just adding them to the rectangle shape (or declaring a new shape based on the rectangle) would restrict these new anchors only to this shape while they are practical for all rectangular shapes.

In this case, all nodes will always have all generated generic anchors (for a specific scope).

This does not add the horizontal line, for this to be part of a shape, we will indeed have to declare a new shape. (We could also cheat again and adjust the shape's path on-the-fly but at that point a proper shape declaration is much cleaner.) You might want to learn about \deferredanchor which allows more flexibility in anchors.

However, the various circuit shapes of TikZ und circuitkz are creating one shape with multiple instances with different numbers of, say, input anchors differently. These also draw some extra bit (a circle) if the input is negated.

You will need to adapt them to include the straight lines as part of the dynamically generated shape variations.

Code

\documentclass[tikz]{standalone}
\makeatletter
\pgfset{
  @generate anchor/.code args={#1:#2:#3:#4}{%
    \pgfdeclaregenericanchor{#1}{%
      \pgfpointlineattime{#2}
        {\pgf@sh@reanchor{##1}{#3}}{\pgf@sh@reanchor{##1}{#4}}}},
  generate anchors/.style n args={3}{
    /utils/tempa/.style={/pgf/@generate anchor={##1:#2:#3}},
    /utils/tempa/.list={#1}}}
\makeatother
\pgfset{generate anchors={in 1/2:.3333, in 2/2:.6667}{north west}{south west}}
\begin{document}
\begin{tikzpicture}
\node[draw, anchor=in 2/2] at (1, 0) (A) {A};
\draw (0,0) -- (A.in 2/2);
\end{tikzpicture}

\begin{tikzpicture}[
  shape example/.style={
    color=black!30, draw, fill=yellow!30,
    line width=+.5cm, inner xsep=+2.5cm, inner ysep=0.5cm},
  generate anchors=
    {in 1/3:.25, in 2/3:.5, in 3/3:.75}{north west}{south west},
  generate anchors=
    {out 1/4:.2, out 2/4:.4, out 3/4:.6, out 4/4:.8}{north east}{south east}]
\node[shape example] (s) {In Out Rectangle\vrule width 1pt height 2cm};
\foreach \anchor/\placement in
  {north west/above left, north/above, north east/above right,
%   west/left, center/above, east/right,
%   mid west/right, mid/above, mid east/left,
%   base west/left, base/below, base east/right,
   south west/below left, south/below, south east/below right,
%   text/left, 10/right, 130/above,
   {in 1/3}/left, {in 2/3}/left, {in 3/3}/left,
   {out 1/4}/right, {out 2/4}/right, {out 3/4}/right, {out 4/4}/right}
   \draw[shift=(s.\anchor)] plot[mark=x] coordinates{(0,0)}
     node[\placement] {\scriptsize\texttt{(s.\anchor)}};
\end{tikzpicture}

\begin{tikzpicture}[
    my anchors/.style = {
            generate anchors={in 1/2:.3333, in 2/2:.6667}{north west}{south west},
            generate anchors={mid 1/2:.3333, mid 2/2:.6667}{north}{south},
            generate anchors={out 1/2:.3333, out 2/2:.6667}{north east}{south east},
        }
    ]
    \node[draw, my anchors, anchor = mid 1/2] (a) at (0,0) {A};
    \node[draw, my anchors, anchor = mid 2/2] (b) at (1,0) {B};
    \draw[my anchors] (a.out 1/2) -- (b.in 2/2);
\end{tikzpicture}
\end{document}

Output

enter image description here

I'll say it again: The following rectangle also has the in 1/2 and in 2/2 anchors available that are defined in the preamble.
enter image description here

enter image description here

4
  • Great thing, I have to learn it! (And I now see that probably mu hack is a bit off due to the linewidth). (BTW: is \pgfdeclaregenericanchor described somewhere?)
    – Rmano
    Commented Jul 25, 2023 at 17:12
  • @Rmano Yes, you'll need the outer sep included as well. I guess, only the source itself describes it. Searching globally on GitHub, it seems to be used by the Forest and PGFPlots once each. Commented Jul 26, 2023 at 8:58
  • This looks great! I'm travelling for a bit but will accept this as soon as I've had a chance to play around with it.
    – N. Virgo
    Commented Jul 26, 2023 at 11:03
  • Sorry for the long delay in accepting, I'm only now getting back to this project. This version works well for what I need - I like each node having inputs like "in 1/3" instead of having to decide how many inputs a node has when you create the node. I've added an additional example, because it wasn't obvious to me that the wire connecting the nodes needs to have "generate anchors" in its style, as well as the nodes themselves.
    – N. Virgo
    Commented Nov 23, 2023 at 6:05
5

Notice that any of the two @Qrrbrbirlbel's answers here and here is much better than this hack...

I leave this answer here as an exercise on how to compute a border anchor.

You can use the border anchors if the node is a square (or with fixed proportions). In this case, the point on the right side at 2/3 height is at an angle of 198.435° from the horizontal, so:

\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{calc}
\begin{document}

\begin{tikzpicture}
    \node[draw] at (1,0) (A) {A};
    \coordinate (connection) at ($(A.north west)!0.66!(A.south west)$);
    \path[draw] (0,0) -- (connection);
    % if the node is a square, use the border anchor:
    % atan( (1/6)/(1/2) \approx 18.435
    \draw (2,0) -- ++(1,0) node[draw, anchor=198.435]{A};
\end{tikzpicture}

\end{document}

enter image description here

If the node shape is not fixed, that's a much more complex (I do not know if the node size is available for the border angle calculation).

It's trivial, but for completeness:

enter image description here

If the node text size is not known, I discovered that it's possible to access the node's box from within the anchors; the following (hack?) seems to work, although it seems that there is still a bit of inaccuracy in the positioning of the anchor:

\documentclass[border=10pt]{standalone}
\usepackage[T1]{fontenc}
\usepackage{tikz}
\usetikzlibrary{calc}
\newcommand\anchorat[1]{%
    % this will fail is "minimum width" or "minimum height" is in force
    \fpeval{180+atand(
                ((#1)-0.5)*(\ht\pgfnodeparttextbox+\dp\pgfnodeparttextbox+
                2*\pgfkeysvalueof{/pgf/inner ysep}+2*\pgfkeysvalueof{/pgf/outer ysep})/
                (0.5*(\wd\pgfnodeparttextbox+
                2*\pgfkeysvalueof{/pgf/inner xsep}+2*\pgfkeysvalueof{/pgf/outer xsep}))
            )}
}
\newcommand{\markpoint}[2][red]{%
    \draw [#1] #2 --++(-.1,-.1) -- ++(.2,.2) ++(-.2,0) -- ++(.2,-.2);
}
\begin{document}
\begin{tikzpicture}[every node/.append style={draw=gray, align=center,
                    line width=1mm, inner sep=1mm}]
        \draw (0,0) -- ++(1,0)
            node[anchor=\anchorat{1/3}] (A) {A};
        \draw (0,-2) -- ++(1,0)
            node[anchor=\anchorat{2/3}] (B) {Alfa Beta Delta\\ Gamma \\ Epsilon\vrule width 1pt height 3pt depth 8pt };
        \markpoint{($(A.north west)!{1/3}!(A.south west)$)}
        \markpoint{($(B.north west)!{2/3}!(B.south west)$)}
        \markpoint[blue]{($(B.north west)!{0}!(B.south west)$)}
        \markpoint[blue]{($(B.north west)!{1}!(B.south west)$)}
        \markpoint[green]{(B.text)}
        \markpoint[green]{(B.center)}
\end{tikzpicture}
\end{document}

enter image description here

It should be quite easy to change the \anchorat macro to specify any point in the border...

I followed the code that defines the size of the rectangle shape, and I am almost sure that the formulas here are correct (at least, they work for 0.0, 0.5, 1.0). Still, there is a clear error (although small) here. Still investigation.

2
  • This is great if the node's aspect ratio is known (+1), but in my case that isn't known, because both the width and height can depend on the content, which is user input.
    – N. Virgo
    Commented Jul 24, 2023 at 1:43
  • @N.Virgo I think I found a way....
    – Rmano
    Commented Jul 25, 2023 at 10:57
3

Here's a shape inout rectangle that supports multiple input and output anchors and the straight lines that come with it.

For now, only a maximum of 10 of those connections are supported. (The circuit shapes of TikZ allow for up to 1024 and also add the needed anchor definitions on-the-fly which I did not implement.) You will need to adjust the 10 yourself if you need more.

A few things to consider:

  • Requesting an input or output anchor number that's above the specified number of anchors only raises a warning and not an error. It successfully returns a point but outside of the shape.

  • PGF adds half the line width on the ends of the small straights (which is barely noticeable in the first example and hidden due to the annotations in the second).

  • You can use the /tikz/inout rectangle/every in out style to change the output of all of those lines.
    In addition, for each of those line the following styles are selectively tried:

    • /tikz/inout rectangle/every in and
    • /tikz/inout rectangle/every out whih allow for different styles for input and output lines as well as
    • /tikz/inout rectangle/in 1, /tikz/inout rectangle/in 2, … and the same for out which enables the customization of each line separately.
  • The \pgscope\begingroup … \tikz@finish\endpgfscope is basically a TikZ \path …;. It might be better to actually use such a \path but I didn't notice any drawbacks in this example.

  • out 2/.style={Circle[open, length=+0pt +3, sep=+0pt +-.5]}- is obviously a bad user-interface to implement a negated output. If this is needed, we can implement something similar to what the circuit libraries of TikZ do.

  • The undocumented \pgfmathloop … \repeatpgfmathloop is used which offers a nice compromise between speed (over \foreach) and convenience (over loops that don't come with their own counter).

Code

\documentclass[tikz]{standalone}
\usetikzlibrary{arrows.meta}
\makeatletter
\tikzset{% this is explicitly a TikZ shape
  inout rectangle/in  length/.initial=1cm,
  inout rectangle/out length/.initial=1cm,
  inout rectangle/every in out/.style=draw,
  inout rectangle/inputs/.initial=3,
  inout rectangle/outputs/.initial=3,
  inout rectangle/.code=\tikzset{shape=inout rectangle,inout rectangle/.cd,#1}}
\def\pgf@sh@inoutrectangle@test#1#2#3{\ifnum#1>#2\relax\pgfwarning{#3put #1\space not defined for this node.}\fi}
\pgfmathloop
  \expandafter\edef\csname pgf@anchor@inout rectangle@input \pgfmathcounter'\endcsname{%
    \noexpand\pgf@sh@inoutrectangle@test{\pgfmathcounter}{\noexpand\inputs}{In}%
    \noexpand\pgfpointlineattime{\pgfmathcounter/(\noexpand\inputs+1)}
      {\noexpand\pgf@sh@reanchor{inout rectangle}{north west}}
      {\noexpand\pgf@sh@reanchor{inout rectangle}{south west}}}%
  \expandafter\edef\csname pgf@anchor@inout rectangle@input \pgfmathcounter\endcsname{%
    \noexpand\pgf@sh@inoutrectangle@test{\pgfmathcounter}{\noexpand\inputs}{In}%
    \noexpand\pgfpointadd{\noexpand\pgf@sh@reanchor{inout rectangle}{input \pgfmathcounter'}}{%
      \noexpand\inout\pgf@x=-\pgf@y \pgf@y=0pt }}%
  \expandafter\edef\csname pgf@anchor@inout rectangle@output \pgfmathcounter'\endcsname{%
    \noexpand\pgf@sh@inoutrectangle@test{\pgfmathcounter}{\noexpand\outputs}{Out}%
    \noexpand\pgfpointlineattime{\pgfmathcounter/(\noexpand\outputs+1)}
      {\noexpand\pgf@sh@reanchor{inout rectangle}{north east}}
      {\noexpand\pgf@sh@reanchor{inout rectangle}{south east}}}%
  \expandafter\edef\csname pgf@anchor@inout rectangle@output \pgfmathcounter\endcsname{%
    \noexpand\pgf@sh@inoutrectangle@test{\pgfmathcounter}{\noexpand\outputs}{Out}%
    \noexpand\pgfpointadd{\noexpand\pgf@sh@reanchor{inout rectangle}{output \pgfmathcounter'}}{%
      \noexpand\inout\pgf@x=\pgf@x \pgf@y=0pt }}%
  \ifnum\pgfmathcounter<10\relax % max number of anchors: 10
\repeatpgfmathloop
\pgfdeclareshape{inout rectangle}{%
  \savedanchor\inout{% it's not a real anchor, it just saves two lengths in one macro
    \pgfpoint{\pgfkeysvalueof{/tikz/inout rectangle/in length}}%
             {\pgfkeysvalueof{/tikz/inout rectangle/out length}}}%
  \savedmacro\inputs {\pgfmathtruncatemacro\inputs {\pgfkeysvalueof{/tikz/inout rectangle/inputs}}}%
  \savedmacro\outputs{\pgfmathtruncatemacro\outputs{\pgfkeysvalueof{/tikz/inout rectangle/outputs}}}%
  \inheritsavedanchors[from=rectangle]\inheritanchor[from=rectangle]{base}%
  \inheritanchor[from=rectangle]{center}\inheritanchor[from=rectangle]{mid}
  \inheritanchor[from=rectangle]{north}\inheritanchor[from=rectangle]{south}%
  \inheritanchor[from=rectangle]{west}\inheritanchor[from=rectangle]{east}%
  \inheritanchor[from=rectangle]{mid west}\inheritanchor[from=rectangle]{base west}%
  \inheritanchor[from=rectangle]{north west}\inheritanchor[from=rectangle]{south west}%
  \inheritanchor[from=rectangle]{mid east}\inheritanchor[from=rectangle]{base east}%
  \inheritanchor[from=rectangle]{north east}\inheritanchor[from=rectangle]{south east}%
  \inheritanchorborder[from=rectangle]\inheritbackgroundpath[from=rectangle]%
  \behindbackgroundpath{% the lines are drawn behind the normal rectangle
    \ifnum\inputs>0
      \pgfmathloop
        \pgfscope\begingroup
          \tikzset{
            inout rectangle/every in out, inout rectangle/every in/.try,
            inout rectangle/in \pgfmathcounter/.try}%
          \pgfpathmoveto{\pgf@sh@reanchor{inout rectangle}{input \pgfmathcounter'}}%
          \pgfpathlineto{\pgf@sh@reanchor{inout rectangle}{input \pgfmathcounter}}%
        \tikz@finish\endpgfscope
        \ifnum\pgfmathcounter<\inputs\relax
      \repeatpgfmathloop
    \fi
    \ifnum\outputs>0
      \pgfmathloop
       \pgfscope\begingroup
         \tikzset{
            inout rectangle/every in out,  inout rectangle/every out/.try,
            inout rectangle/out \pgfmathcounter/.try}%
          \pgfpathmoveto{\pgf@sh@reanchor{inout rectangle}{output \pgfmathcounter'}}%
          \pgfpathlineto{\pgf@sh@reanchor{inout rectangle}{output \pgfmathcounter}}%
        \tikz@finish\endpgfscope
        \ifnum\pgfmathcounter<\outputs\relax
      \repeatpgfmathloop
    \fi
  }%
}
\makeatother
\begin{document}
\begin{tikzpicture}
\node[
  draw, inout rectangle={inputs=2, in 1/.style=path only, outputs=0},
  anchor=input 2'] (A) {A};
\end{tikzpicture}

\begin{tikzpicture}[
  shape example/.style={
    color=black!30, draw, fill=yellow!30,
    line width=+.5cm, inner xsep=+2.5cm, inner ysep=0.5cm}]
\node[
  shape example,
  inout rectangle={
    inputs=3,
    outputs=4,
    every in out/.append style={line width/.expanded=\the\dimexpr.5\pgflinewidth},
    out 2/.style={Circle[open, length=+0pt +3, sep=+0pt +-.5]}-
  }] (s) {Inout Rectangle\vrule width 1pt height 2cm};
\foreach \anchor/\placement in
  {north west/above left, north/above, north east/above right,
   south west/below left, south/below, south east/below right,
   input 1/left, input 2/left, input 3/left,
   input 1'/right, input 2'/right, input 3'/right,
   output 1/right, output 2/right, output 3/right, output 4/right,
   output 1'/left, output 2'/left, output 3'/left, output 4'/left}
   \draw[shift=(s.\anchor)] plot[mark=x] coordinates{(0,0)}
     node[\placement] {\scriptsize\texttt{(s.\anchor)}};
\end{tikzpicture}
\end{document}

Output

enter image description here

enter image description here

1
  • Another dimension would be to allow for different lengths for each input/output but I hope this won't be needed. Commented Jul 26, 2023 at 13:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .