我使用 TikZ 绘制(无向)图,但有时我希望能够标记所有交叉点/交叉点的位置自动地。我知道 TikZintersections
库有name intersections=
密钥,但这需要命名图中的所有路径,然后对每对相交的路径使用该密钥(如果我需要修改图,这将非常痛苦,因为相交的边对可能会改变。)手动命名所有路径也会使代码的可读性显著降低。
问:有没有办法可以自动完成此操作?
例如,以下是彼得森图在 TikZ 中:
\documentclass{standalone}
\usepackage{tikz}
\tikzset{graph/.style={
every node/.style={circle,fill=black,inner sep=0pt,minimum width=8pt},
every path/.style={thick}
}}
\begin{document}
\begin{tikzpicture}[graph]
\foreach \i [evaluate=\i as \a using 90-(\i-1)*360/5] in {1,...,5} {
\node (o\i) at (\a:2) {};
\node (i\i) at (\a-36:0.7) {};
}
\draw (o1) -- (o2) -- (o3) -- (o4) -- (o5) -- (o1);
\draw (i1) -- (i2) -- (i3) -- (i4) -- (i5) -- (i1);
\draw (o1) -- (i3); \draw (o2) -- (i1); \draw (o5) -- (i5);
\draw[bend left] (o3) to (i4);
\draw[bend right] (o4) to (i2);
\end{tikzpicture}
\end{document}
此嵌入中有两个交叉点。手动标记交叉点需要执行以下操作:
\documentclass{standalone}
\usepackage{tikz}
\usetikzlibrary{intersections}
\tikzset{graph/.style={
every node/.style={circle,fill=black,inner sep=0pt,minimum width=8pt},
every path/.style={thick},
crossing/.style={rectangle,fill=red,inner sep=0pt,minimum size=4pt}
}}
\begin{document}
\begin{tikzpicture}[graph]
\foreach \i [evaluate=\i as \a using 90-(\i-1)*360/5] in {1,...,5} {
\node (o\i) at (\a:2) {};
\node (i\i) at (\a-36:0.7) {};
}
\draw (o1) -- (o2) -- (o3) -- (o4) -- (o5) -- (o1);
\draw[name path=p1] (i1) -- (i2) -- (i3) -- (i4) -- (i5) -- (i1);
\draw[name path=p2] (o1) -- (i3);
\draw (o2) -- (i1); \draw (o5) -- (i5);
\draw[name path=p3,bend left] (o3) to (i4);
\draw[name path=p4,bend right] (o4) to (i2);
\path [name intersections={of=p1 and p2}] (intersection-1) node[crossing] {};
\path [name intersections={of=p3 and p4}] (intersection-1) node[crossing] {};
\end{tikzpicture}
\end{document}
输出结果如下:
您可能可以想象,对于更复杂的图形,这种方法会遇到可读性问题;有时我最终不得不命名绘图中的几乎所有路径和name intersections
数十对路径。理想的情况是,如果我可以只将类似的东西添加到环境样式showcrossings
中tikzpicture
并让它自动发生。
我将发布我自己的解决方案,但如果有人可以改进我的方法或有不同的方法,我会感兴趣!
答案1
这是我的解决方案。希望其他人会觉得它有用。
归结为:自动命名图形中的每条路径,然后遍历每对命名路径并将节点放置在发现的任何交叉点处。
我在此过程中发现了一些问题:
起初我想到使用
every path/.style={...}
或某物来自动命名边。不幸的是,\node
我用来创建顶点的命令最终会隐式创建额外的路径,而我们不想找到与这些路径的交点。此外,对于更复杂的图形,我有时会使用(未绘制的)路径来放置顶点或辅助坐标,如果我使用 ,这些会干扰every path
。为了解决这个问题,我定义了一个\edge
命令,用它来替换我绘制的所有\path
命令。它与 相同,\draw
但应用了一种edge
我们可以定义的样式。\newcommand*\edge[1][]{\draw[edge,#1]}
我们需要一个计数器来记录绘图中命名路径的数量。趁着这个机会,我们再创建一个计数器,这样我们就可以存储找到的交叉点数量,以防我们出于某种原因对此感兴趣(例如,我们可以在文档中使用该输出)。
\newcounter{ngraphpaths} \newcounter{ncrossings}
在定义
showcrossings
样式时,我们使每条边增加计数器并使用name path global
。我最终使用了全局,因为否则,如果在\foreach
或其他代码块内添加边,它们在我们去寻找交点的图片块末尾是不可见的。我假设这cross_p
是一个足够独特的前缀,不会与其他东西发生冲突。我还检查了,这甚至适用于同一文件中的多个图形;库intersections
只做了一个\global\let
,所以覆盖以前的名字没有问题。edge/.append style={increment_ngraphpaths,name path global={cross_p\the\value{ngraphpaths}}}, increment_ngraphpaths/.code={\addtocounter{ngraphpaths}{1}},
我们还需要确保在每张图片开始时重置计数器:
execute at begin picture={\setcounter{ngraphpaths}{0}\setcounter{ncrossings}{0}},
最后,在图片的末尾,如果至少有 2 条路径具有边缘样式,我们会迭代每个可能的对(相等对除外* - 请参阅下面的注释)并寻找交点。令人惊讶的是,尽管是 O(n²),但我并没有发现这对性能有太大的影响。请注意,在尝试使用 迭代它们之前,我们必须检查至少有一个交点
\foreach
。出于记录目的,我们还将找到的交叉点数量打印到控制台 - 这在实践中很有用,因为在尝试绘制图形时,我们通常已经知道交叉点的数量应该是多少。execute at end picture={% \ifnum \value{ngraphpaths}>1 \pgfmathtruncatemacro\aend{\value{ngraphpaths}-1} \foreach \a in {1,...,\aend}{ \pgfmathtruncatemacro\bstart{\a+1} \foreach \b in {\bstart,...,\value{ngraphpaths}} \path [name intersections={of={cross_p\a} and {cross_p\b},name=i,total=\t}] \ifnum \t>0 \foreach \s in {1,...,\t}{(i-\s) node[crossing] {}} \pgfextra{\addtocounter{ncrossings}{\t}} \fi; } \fi \typeout{showcrossings: found \the\value{ncrossings} crossing(s)}}
综合起来:
\documentclass{standalone}
\usepackage{tikz}
\usetikzlibrary{intersections}
\tikzset{graph/.style={
every node/.style={circle,fill=black,inner sep=0pt,minimum width=8pt},
edge/.style={thick},
crossing/.style={rectangle,fill=red,inner sep=0pt,minimum size=4pt}
}}
\newcommand*\edge[1][]{\draw[edge,#1]}
\newcounter{ngraphpaths}
\newcounter{ncrossings}
\tikzset{showcrossings/.style={
edge/.append style={increment_ngraphpaths,name path global={cross_p\the\value{ngraphpaths}}},
increment_ngraphpaths/.code={\addtocounter{ngraphpaths}{1}},
execute at begin picture={\setcounter{ngraphpaths}{0}\setcounter{ncrossings}{0}},
execute at end picture={%
\ifnum \value{ngraphpaths}>1
\pgfmathtruncatemacro\aend{\value{ngraphpaths}-1}
\foreach \a in {1,...,\aend}{
\pgfmathtruncatemacro\bstart{\a+1}
\foreach \b in {\bstart,...,\value{ngraphpaths}}
\path [name intersections={of={cross_p\a} and {cross_p\b},name=i,total=\t}]
\ifnum \t>0
\foreach \s in {1,...,\t}{(i-\s) node[crossing] {}}
\pgfextra{\addtocounter{ncrossings}{\t}}
\fi;
}
\fi
\typeout{showcrossings: found \the\value{ncrossings} crossing(s)}}
}}
\begin{document}
\begin{tikzpicture}[graph,showcrossings]
\foreach \i [evaluate=\i as \a using 90-(\i-1)*360/5] in {1,...,5} {
\node (o\i) at (\a:2) {};
\node (i\i) at (\a-36:0.7) {};
}
\edge (o1) -- (o2) -- (o3) -- (o4) -- (o5) -- (o1);
\edge (i1) -- (i2) -- (i3) -- (i4) -- (i5) -- (i1);
\edge (o1) -- (i3);
\edge (o2) -- (i1); \edge (o5) -- (i5);
\edge[bend left] (o3) to (i4);
\edge[bend right] (o4) to (i2);
\end{tikzpicture}
\end{document}
*关于自交叉路径的说明
在上面的代码中,我们从未检查命名路径cross_p\a
与和cross_p\b
的相交值是否相等。尽管可以创建一条与自身相交的路径。如果这样做,上面的代码将无法检测到交叉点。如果重写 foreach 循环,使开始于而不是,则可以找到这些交叉点,但在我的测试中,这仅适用于直线路径。一旦有一些弯曲的路径,它就会中断,并且交叉点库将找到数百个误报交叉点。\a
\b
\edge (1) -- (2) -- (3) ...
\b
\a
\a+1
简而言之,如果您使用此功能,请确保每个\edge
命令在路径上不包含任何自交叉。