![在 TikZ 中绘制递归结构的迭代/使用循环中定义的节点名称](https://linux22.com/image/267894/%E5%9C%A8%20TikZ%20%E4%B8%AD%E7%BB%98%E5%88%B6%E9%80%92%E5%BD%92%E7%BB%93%E6%9E%84%E7%9A%84%E8%BF%AD%E4%BB%A3%2F%E4%BD%BF%E7%94%A8%E5%BE%AA%E7%8E%AF%E4%B8%AD%E5%AE%9A%E4%B9%89%E7%9A%84%E8%8A%82%E7%82%B9%E5%90%8D%E7%A7%B0.png)
这是我第一次使用 TikZ,我正在尝试绘制一些与希尔伯特 空间填充曲线,基于最近的一篇热门文章(HTML,PDF)具体来说,我试图画出曲线的前几次迭代,节点用四元字符串标记(字母表 {0,1,2,3} 上的字符串)。我有一些可以工作的方法,但我想知道是否有更好的方法。第一次迭代很简单——我可以得到这个图像:
使用以下代码:
\begin{tikzpicture}
\node (0) at (0, 0) {$0$};
\node (1) at (0, 1) {$1$};
\node (2) at (1, 1) {$2$};
\node (3) at (1, 0) {$3$};
\draw [->] (0) -- (1);
\draw [->] (1) -- (2);
\draw [->] (2) -- (3);
\end{tikzpicture}
第二次迭代涉及到四个副本,其中第一个和最后一个被翻转和旋转,我得到了这个图像:
使用以下代码:
\begin{tikzpicture}
\foreach \quadrant / \xscale / \rotate / \xshift / \yshift in
{0 / -1 / 90 / 0 / 0,
1 / +1 / 00 / 0 / 2,
2 / +1 / 00 / 2 / 2,
3 / -1 / -90 / 3 / 1} % (3, 1) = (1, 1) + (2, 0): the former is just to rotate
{
\begin{scope}[shift = {(\xshift, \yshift)}, xscale = \xscale, rotate = \rotate]
\node (\quadrant0) at (0, 0) {$\quadrant0$};
\node (\quadrant1) at (0, 1) {$\quadrant1$};
\node (\quadrant2) at (1, 1) {$\quadrant2$};
\node (\quadrant3) at (1, 0) {$\quadrant3$};
\draw [->] (\quadrant0) -- (\quadrant1);
\draw [->] (\quadrant1) -- (\quadrant2);
\draw [->] (\quadrant2) -- (\quadrant3);
\end{scope}
}
\draw [->] (0, 1) -- (0, 2);
\draw [->] (1, 2) -- (2, 2);
\draw [->] (3, 2) -- (3, 1);
\end{tikzpicture}
存在一个实际问题和几个代码清洁度问题:
跨越不同象限的箭头(代码中的最后三行)看起来很丑陋,因为它们是在坐标之间绘制的,而不是在节点之间绘制的。我尝试使用类似代码在节点之间绘制它,
\draw [->] (03) -- (10);
但我收到错误消息,例如“错误:包 pgf 错误:没有已知的名为 03 的形状。”所以我猜想在 foreach 循环中,即使\quadrant
值为0
,该行\node (\quadrant3) at (1, 0) {$\quadrant3$};
要么实际上没有创建名为 的节点03
,要么在循环外无法访问。如何在循环外使用与 对应的节点03
?有没有比我所做的更好的方法来绘制图形?(例如翻转+旋转的更好方法?)
在 foreach 循环中,我刚刚
\quadrant
在上图的代码中为每个节点添加了前缀;有没有办法重用该代码(可能首先以更通用的方式编写它,将前缀设置为空字符串)而不是重复它?
第三次迭代有第二次迭代的四个副本,除了现在丑陋的箭头数量更多之外,没有新的问题,而且我不得不基本上重复第二次迭代的所有代码(只是q
在大多数变量名前面加上前缀)。
\begin{tikzpicture}
\foreach \qquadrant / \qxscale / \qrotate / \qxshift / \qyshift in
{0 / -1 / 90 / 0 / 0,
1 / +1 / 00 / 0 / 4,
2 / +1 / 00 / 4 / 4,
3 / -1 / -90 / 7 / 3} % (7, 3) = (3, 3) + (4, 0)
{
\begin{scope}[shift = {(\qxshift, \qyshift)}, xscale = \qxscale, rotate = \qrotate]
\foreach \quadrant / \xscale / \rotate / \xshift / \yshift in
{0 / -1 / 90 / 0 / 0,
1 / +1 / 00 / 0 / 2,
2 / +1 / 00 / 2 / 2,
3 / -1 / -90 / 3 / 1} % (3, 1) = (1, 1) + (2, 0): the former is just to rotate
{
\begin{scope}[shift = {(\xshift, \yshift)}, xscale = \xscale, rotate = \rotate]
\node (\qquadrant\quadrant0) at (0, 0) {$\qquadrant\quadrant0$};
\node (\qquadrant\quadrant1) at (0, 1) {$\qquadrant\quadrant1$};
\node (\qquadrant\quadrant2) at (1, 1) {$\qquadrant\quadrant2$};
\node (\qquadrant\quadrant3) at (1, 0) {$\qquadrant\quadrant3$};
\draw [->] (\qquadrant\quadrant0) -- (\qquadrant\quadrant1);
\draw [->] (\qquadrant\quadrant1) -- (\qquadrant\quadrant2);
\draw [->] (\qquadrant\quadrant2) -- (\qquadrant\quadrant3);
\end{scope}
}
\draw [->] (0, 1) -- (0, 2);
\draw [->] (1, 2) -- (2, 2);
\draw [->] (3, 2) -- (3, 1);
\end{scope}
}
\draw [->] (0, 3) -- (0, 4);
\draw [->] (3, 4) -- (4, 4);
\draw [->] (7, 4) -- (7, 3);
\end{tikzpicture}
我不期望绘制第三次迭代之后的图形(由于节点名称很长,水平箭头已经变得非常短),并且我愿意忍受代码重复,因此只需修复这三个图形的箭头就足够了。
编辑:我正在寻找一种传统的编程解决方案,但多亏了答案/评论,我了解了 Lindenmayer 系统,它看起来非常迷人。我接受了答案,并希望更详细地探索这些系统。
同时,当我尝试为上述问题再举一个最小的例子时,我自己找到了答案。结果发现答案非常简单:它与 中的数字和斜线之间的空格有关。\foreach \quadrant / \xscale [...] in {0 / -1 [...]
因此,循环内的节点被创建为0 3
而不是03
。将0 /
etc. 更改为0/
etc. 得到下图:
使用下面的代码(我现在知道如何编写得更好,但为了对比或相似性,保持与上面的代码相似):
\begin{tikzpicture}
\foreach \qquadrant / \qxscale / \qrotate / \qxshift / \qyshift in
{0/ -1 / 90 / 0 / 0,
1/ +1 / 00 / 0 / 4,
2/ +1 / 00 / 4 / 4,
3/ -1 / -90 / 7 / 3} % (7, 3) = (3, 3) + (4, 0)
{
\begin{scope}[shift = {(\qxshift, \qyshift)}, xscale = \qxscale, rotate = \qrotate]
\foreach \quadrant / \xscale / \rotate / \xshift / \yshift in
{0/ -1 / 90 / 0 / 0,
1/ +1 / 00 / 0 / 2,
2/ +1 / 00 / 2 / 2,
3/ -1 / -90 / 3 / 1} % (3, 1) = (1, 1) + (2, 0): the former is just to rotate
{
\begin{scope}[shift = {(\xshift, \yshift)}, xscale = \xscale, rotate = \rotate]
\node (\qquadrant\quadrant0) at (0, 0) {$\qquadrant\quadrant0$};
\node (\qquadrant\quadrant1) at (0, 1) {$\qquadrant\quadrant1$};
\node (\qquadrant\quadrant2) at (1, 1) {$\qquadrant\quadrant2$};
\node (\qquadrant\quadrant3) at (1, 0) {$\qquadrant\quadrant3$};
\draw [->] (\qquadrant\quadrant0) -- (\qquadrant\quadrant1);
\draw [->] (\qquadrant\quadrant1) -- (\qquadrant\quadrant2);
\draw [->] (\qquadrant\quadrant2) -- (\qquadrant\quadrant3);
\end{scope}
}
\draw [->] (\qquadrant03) -- (\qquadrant10);
\draw [->] (\qquadrant13) -- (\qquadrant20);
\draw [->] (\qquadrant23) -- (\qquadrant30);
\end{scope}
}
\draw [->] (033) -- (100);
\draw [->] (133) -- (200);
\draw [->] (233) -- (300);
\end{tikzpicture}
答案1
正如 Marc 指出的那样,Lindenmayer 系统(2.10 手册中的第 37 节“Lindenmayer 系统绘图库”)可以做到这一点。但是,需要进行一些调整才能使节点文本正确,为此我使用了基础转换工具(第 64.4 节“基础转换”)。
但请注意,一些多余的空格从某个地方悄悄混入了文档中,但我不确定在哪里。
\documentclass[border=0.125cm]{standalone}
\usepackage{tikz}
\usetikzlibrary{lindenmayersystems}
\newcount\hilbertnodecount
\newcount\hilbertnodepreviouscount
\pgfdeclarelindenmayersystem{hilbert curve}{
% Symbol to initialise things
\symbol{I}{\hilbertnodecount=0}
% Symbol to draw node
\symbol{X}{%
\pgflsystemdrawforward%
\node [every hilbert node/.try, hilbert node \the\hilbertnodecount/.try ]
(hilbert-\the\hilbertnodecount) {};
\ifnum\hilbertnodecount>0\relax%
\hilbertnodepreviouscount=\hilbertnodecount%
\advance\hilbertnodepreviouscount by-1\relax%
\path [hilbert node path/.try] (hilbert-\the\hilbertnodepreviouscount) -- (hilbert-\the\hilbertnodecount);
\fi%
\advance\hilbertnodecount by1%
}
% Symbol to turn left
\symbol{+}{\pgflsystemturnright}
% Symbol to turn right
\symbol{-}{\pgflsystemturnleft}
% Initial rule (to reset \hilbertnodecount and draw first node)
\rule{S -> IXA}
% Rules to draw Hilbert curve
\rule{A -> +BX-AXA-XB+}
\rule{B -> -AX+BXB+XA-}
}
\makeatletter
% Need a teensy hack to get the order
\def\tikzlsystemorder{\pgf@lsystem@order}%
\makeatother
\tikzset{
hilbert nodes/.style={
lindenmayer system={hilbert curve, axiom=S, order=#1, angle=90},
insert path={lindenmayer system}
},
hilbert step/.style={%
/pgf/lindenmayer system/step=#1
}
}
\tikzset{%
every hilbert node/.style={
draw=none,
%minimum size=0.75cm, <- change minimum size
%font=\tiny, <- change font
execute at end node={% <- code for printing number
% Get the number of digits
\pgfmathparse{int(\tikzlsystemorder-1)}%
\let\ndigits=\pgfmathresult%
% Ensure result of base conversion are padded with digits%
\pgfmathsetbasenumberlength{\ndigits}%
% Convert the node number to base 4
\pgfmathdectobase\n{\hilbertnodecount}{4}%
\n%
}
},
hilbert step=1cm,% <- distance between nodes
hilbert node path/.style={draw, ->}% <- style for lines between nodes
}
\begin{document}
\begin{tabular}{c} %
\tikz[yscale=-1]\path [hilbert nodes=2]; \\[1cm]
\tikz[yscale=-1]\path [hilbert nodes=3]; \\[1cm]
\tikz[yscale=-1]\path [hilbert nodes=4];
\end{tabular}
\end{document}
答案2
这是一个基于此的解决方案例子texample.net 的代码没有优化,\foreach \i in {4,8,12,16} ...
而且我使用了太多的计数器:(
更新
我删除了\foreach \i in {4,8,12,16} ...
\documentclass{standalone}
\usepackage{tikz}
\pgfmathsetmacro\HilbertLastX{0}
\pgfmathsetmacro\HilbertLastY{0}
\newcounter{Order}
\newcounter{Num}
\newcounter{HilbertOrder}
\newcounter{GlobalHilbertOrder}
\tikzset{hilbert/.style={minimum height=1.5ex,minimum width=3ex,inner sep=0pt,outer sep=0pt,font=\tiny}}
\def\AddNewNode#1#2{%
\pgfmathsetmacro\HilbertLastX{\HilbertLastX+#1}
\pgfmathsetmacro\HilbertLastY{\HilbertLastY+#2}
\addtocounter{Num}{1}
\node[hilbert] (tmp) at (\HilbertLastX cm,\HilbertLastY cm) {};
\draw[red,->] (last) -- (tmp);
\pgfmathsetbasenumberlength{3}%
\pgfmathdectobase\mynumber{\theNum}{4}
\node[hilbert] (last) at (\HilbertLastX cm,\HilbertLastY cm) {\mynumber};
}%
\def\Hilbert[#1,#2,#3,#4,#5,#6,#7,#8] {
\ifnum\value{HilbertOrder} > 0%
\addtocounter{HilbertOrder}{-1}
\Hilbert[#5,#6,#7,#8,#1,#2,#3,#4]
\AddNewNode {#1} {#2}
\Hilbert[#1,#2,#3,#4,#5,#6,#7,#8]
\AddNewNode {#5} {#6}
\Hilbert[#1,#2,#3,#4,#5,#6,#7,#8]
\AddNewNode {#3} {#4}
\Hilbert[#7,#8,#5,#6,#3,#4,#1,#2]
\addtocounter{HilbertOrder}{1}
\addtocounter{GlobalHilbertOrder}{1}
\ifnum\value{Order} >1
\pgfmathparse{mod(\value{Order},4)}
\ifdim\pgfmathresult pt =0pt \addtocounter{GlobalHilbertOrder}{-1}\fi
\fi
\addtocounter{Order}{1}
\fi
}%
\def\hilbert((#1,#2),#3){%
\pgfmathsetmacro\HilbertLastX{\HilbertLastX+#1}
\pgfmathsetmacro\HilbertLastY{\HilbertLastY+#2}
\path (\HilbertLastX,\HilbertLastY);
\setcounter{HilbertOrder}{#3}
\Hilbert[1,0,-1,0,0,1,0,-1]
}%
\begin{document}
\parindent=0pt
\begin{tikzpicture}[node distance=0cm,every path/.style={thin}]
\node[hilbert] (last) at (\HilbertLastX cm,\HilbertLastY cm) {000};
\setcounter{GlobalHilbertOrder}{0} \setcounter{Order}{0} \setcounter{Num}{0}
\hilbert((0mm,0mm),3);
\end{tikzpicture}
\end{document}