diff options
author | Norbert Preining <norbert@preining.info> | 2023-04-25 03:01:27 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2023-04-25 03:01:27 +0000 |
commit | 800f3919ea46f82dc11cbfb51f08d2f3a1c43a70 (patch) | |
tree | 0071bb5d56e0551b532c2828c29ebd2ad2d7e340 /graphics/pgf/contrib/tikz-nfold | |
parent | e77721e3571e9341ee83577f574128a9f4e37c38 (diff) |
CTAN sync 202304250301
Diffstat (limited to 'graphics/pgf/contrib/tikz-nfold')
-rw-r--r-- | graphics/pgf/contrib/tikz-nfold/README.md | 2 | ||||
-rw-r--r-- | graphics/pgf/contrib/tikz-nfold/pgflibrarybezieroffset.code.tex | 416 | ||||
-rw-r--r-- | graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.pdf | bin | 289815 -> 302366 bytes | |||
-rw-r--r-- | graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.tex | 222 | ||||
-rw-r--r-- | graphics/pgf/contrib/tikz-nfold/tikzlibrarynfold.code.tex | 1462 |
5 files changed, 1293 insertions, 809 deletions
diff --git a/graphics/pgf/contrib/tikz-nfold/README.md b/graphics/pgf/contrib/tikz-nfold/README.md index 305b8edecb..97d6549f60 100644 --- a/graphics/pgf/contrib/tikz-nfold/README.md +++ b/graphics/pgf/contrib/tikz-nfold/README.md @@ -1,5 +1,5 @@ # tikz-nfold -## Version 0.0.1 +## Version 0.1.0 This library adds higher-order paths to [TikZ](https://ctan.org/pkg/pgf) and also fixes some graphical issues with TikZ' `double` paths, used e.g. in wide arrows. It is also compatible with [tikz-cd](https://ctan.org/pkg/tikz-cd), adding support for triple and higher arrows. See the [documentation](tikz-nfold-doc.pdf) for full details. diff --git a/graphics/pgf/contrib/tikz-nfold/pgflibrarybezieroffset.code.tex b/graphics/pgf/contrib/tikz-nfold/pgflibrarybezieroffset.code.tex index e3391ed92c..7d9a86312f 100644 --- a/graphics/pgf/contrib/tikz-nfold/pgflibrarybezieroffset.code.tex +++ b/graphics/pgf/contrib/tikz-nfold/pgflibrarybezieroffset.code.tex @@ -16,12 +16,6 @@ % This work consists of the files pgflibrarybezieroffset.code.tex, % tikzlibrarynfold.code.tex, tikz-nfold-doc.tex, and tikz-nfold-doc.pdf. - -% stores the current \pgf@x and \pgf@y in #1 -\def\pgfstorepoint#1{\edef#1{\noexpand\pgfpoint{\the\pgf@x}{\the\pgf@y}}} -% global version in case we need it: -\def\pgfglobalstorepoint#1{\xdef#1{\noexpand\pgfpoint{\the\pgf@x}{\the\pgf@y}}} - % Split a Bezier curve (de Casteljau's algorithm) % #1 = time (between 0 and 1) % #2-#5: control points @@ -32,19 +26,16 @@ % this is partially implemented in some pgf file, possibly decorations or basic paths. % But maybe I will need the general case in the future, maybe with some advanced fully simple detection. % Leave it in for now -\newcommand{\pgf@splitbezier}[5]{ +\def\pgf@splitbezier#1#2#3#4#5{% % based on pgfcorepoints.code.tex, \pgfpointcurveattime - \pgfmathparse{#1}% - \let\pgf@time@s=\pgfmathresult% - \global\pgf@x=\pgfmathresult pt% - \global\pgf@x=-\pgf@x% + \pgfmathsetmacro\pgf@time@s{#1}% + \pgf@x=-\pgf@time@s pt% \advance\pgf@x by 1pt% \edef\pgf@time@t{\pgf@sys@tonumber{\pgf@x}}% % P^0_3 - \pgf@process{#5}% + \pgfextract@process\pgf@splitbezier@ii@iv{#5}% \pgf@xc=\pgf@x% \pgf@yc=\pgf@y% - \pgfstorepoint{\pgf@splitbezier@ii@iv} % P^0_2 \pgf@process{#4}% \pgf@xb=\pgf@x% @@ -54,90 +45,123 @@ \pgf@xa=\pgf@x% \pgf@ya=\pgf@y% % P^0_0 - \pgf@process{#2}% - \pgfstorepoint{\pgf@splitbezier@i@i} + \pgfextract@process\pgf@splitbezier@i@i{#2}% % First iteration: % P^1_0 - \global\pgf@x=\pgf@time@t\pgf@x\global\advance\pgf@x by\pgf@time@s\pgf@xa% - \global\pgf@y=\pgf@time@t\pgf@y\global\advance\pgf@y by\pgf@time@s\pgf@ya% - \pgfstorepoint{\pgf@splitbezier@i@ii} + \pgf@x=\pgf@time@t\pgf@x\advance\pgf@x by\pgf@time@s\pgf@xa% + \pgf@y=\pgf@time@t\pgf@y\advance\pgf@y by\pgf@time@s\pgf@ya% + \pgfextract@process\pgf@splitbezier@i@ii{}% % P^1_1 \pgf@xa=\pgf@time@t\pgf@xa\advance\pgf@xa by\pgf@time@s\pgf@xb% \pgf@ya=\pgf@time@t\pgf@ya\advance\pgf@ya by\pgf@time@s\pgf@yb% % P^1_2 \pgf@xb=\pgf@time@t\pgf@xb\advance\pgf@xb by\pgf@time@s\pgf@xc% \pgf@yb=\pgf@time@t\pgf@yb\advance\pgf@yb by\pgf@time@s\pgf@yc% - \edef\pgf@splitbezier@ii@iii{\noexpand\pgfpoint{\the\pgf@xb}{\the\pgf@yb}} + \edef\pgf@splitbezier@ii@iii{\noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}% % P^2_0 - \global\pgf@x=\pgf@time@t\pgf@x\global\advance\pgf@x by\pgf@time@s\pgf@xa% - \global\pgf@y=\pgf@time@t\pgf@y\global\advance\pgf@y by\pgf@time@s\pgf@ya% - \pgfstorepoint{\pgf@splitbezier@i@iii} + \pgf@x=\pgf@time@t\pgf@x\advance\pgf@x by\pgf@time@s\pgf@xa% + \pgf@y=\pgf@time@t\pgf@y\advance\pgf@y by\pgf@time@s\pgf@ya% + \pgfextract@process\pgf@splitbezier@i@iii{}% % P^2_1 \pgf@xa=\pgf@time@t\pgf@xa\advance\pgf@xa by\pgf@time@s\pgf@xb% \pgf@ya=\pgf@time@t\pgf@ya\advance\pgf@ya by\pgf@time@s\pgf@yb% - \edef\pgf@splitbezier@ii@ii{\noexpand\pgfpoint{\the\pgf@xa}{\the\pgf@ya}} + \edef\pgf@splitbezier@ii@ii{\noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}% % P^3_0 - \global\pgf@x=\pgf@time@t\pgf@x\global\advance\pgf@x by\pgf@time@s\pgf@xa% - \global\pgf@y=\pgf@time@t\pgf@y\global\advance\pgf@y by\pgf@time@s\pgf@ya% - \pgfstorepoint{\pgf@splitbezier@i@iv} - \pgfstorepoint{\pgf@splitbezier@ii@i} + \pgf@x=\pgf@time@t\pgf@x\advance\pgf@x by\pgf@time@s\pgf@xa% + \pgf@y=\pgf@time@t\pgf@y\advance\pgf@y by\pgf@time@s\pgf@ya% + \pgfextract@process\pgf@splitbezier@i@iv{}% + \let\pgf@splitbezier@ii@i\pgf@splitbezier@i@iv } % computes the cross product and puts it into \pgfmathresult -\newcommand{\pgfcrossproduct}[2]{ - \pgf@process{#1}% - \pgf@xa=\pgf@x% - \pgf@ya=\pgf@y% - \pgf@process{#2}% - \pgfmathparse{\pgf@xa*\pgf@y-\pgf@ya*\pgf@x}% +\def\pgfmathcrossproduct#1#2{% + \begingroup + \pgf@process{#1}% + \pgf@xa=\pgf@x% + \pgf@ya=\pgf@y% + \pgf@process{#2}% + \pgf@y=\pgf@sys@tonumber\pgf@xa\pgf@y + \advance\pgf@y by -\pgf@sys@tonumber\pgf@ya\pgf@x + \pgfmath@returnone\pgf@y + \endgroup } -\newcommand{\pgfdotproduct}[2]{ - \pgf@process{#1}% - \pgf@xa=\pgf@x% - \pgf@ya=\pgf@y% - \pgf@process{#2}% - \pgfmathparse{\pgf@xa*\pgf@x+\pgf@ya*\pgf@y}% +\def\pgfmathdotproduct#1#2{% + \begingroup + \pgf@process{#1}% + \pgf@xa=\pgf@x% + \pgf@ya=\pgf@y% + \pgf@process{#2}% + \pgf@x=\pgf@sys@tonumber\pgf@xa\pgf@x + \advance\pgf@x by \pgf@sys@tonumber\pgf@ya\pgf@y + \pgfmath@returnone\pgf@x + \endgroup } -\newcommand{\pgfcrossdot}[2]{ - \pgf@process{#1}% - \pgf@xa=\pgf@x% - \pgf@ya=\pgf@y% - \pgf@process{#2}% - \pgfmathsetlengthmacro{\pgf@tmp@dot}{\pgf@xa*\pgf@x+\pgf@ya*\pgf@y}% - \pgfmathsetlengthmacro{\pgf@tmp@cross}{\pgf@xa*\pgf@y-\pgf@ya*\pgf@x}% +\def\pgfmathcrossdot#1#2{% + \begingroup + \pgf@process{#1}% + \pgf@xa=\pgf@x% + \pgf@ya=\pgf@y% + \pgf@process{#2}% + \pgf@xb=\pgf@sys@tonumber\pgf@xa\pgf@x + \pgf@yb=\pgf@sys@tonumber\pgf@xa\pgf@y + \advance\pgf@xb by \pgf@sys@tonumber\pgf@ya\pgf@y + \advance\pgf@yb by -\pgf@sys@tonumber\pgf@ya\pgf@x + \edef\pgf@temp{% + \edef\noexpand\pgf@tmp@dot{\pgf@sys@tonumber\pgf@xb}% + \edef\noexpand\pgf@tmp@cross{\pgf@sys@tonumber\pgf@yb}% + }% + \expandafter + \endgroup\pgf@temp } - +% Calculates abs(\pgf@x) + abs(\pgf@y) in #1 +\def\pgfpointtaxicabnorm#1{% + \ifdim\pgf@x<0pt + #1=-\pgf@x + \else + #1=\pgf@x + \fi + \ifdim\pgf@y<0pt + \advance#1 by -\pgf@y + \else + \advance#1 by \pgf@y + \fi +} % Computes the normalised tangents of a given Bezier curve and stores them in \pgf@tmp@tang@i and \pgf@tmp@tang@ii. +% Also computes the angles and stores them in \pgf@tmp@angle@i and \pgf@tmp@angle@ii. % All degenerate cases are covered. For a triple degenerate curve (all points equal), the vector (1,0) is returned. -\newcommand{\pgf@offset@compute@tangents}[4]{ - \pgfpointdiff{#1}{#2} % unintuitively, this is PTii - PTi - \pgfmathparse{abs(\pgf@x) + abs(\pgf@y)} - \ifdim\pgfmathresult pt<0.1pt\relax +\def\pgf@offset@compute@tangents#1#2#3#4{% + \pgf@process{\pgfpointdiff{#1}{#2}}% unintuitively, this is PTii - PTi + \pgfpointtaxicabnorm\pgf@xa + \ifdim\pgf@xa<0.1pt\relax % edge case: first point and first control point are equal - \pgfpointdiff{#1}{#3} - \pgfmathparse{abs(\pgf@x) + abs(\pgf@y)} - \ifdim\pgfmathresult pt<0.1pt\relax + \pgf@process{\pgfpointdiff{#1}{#3}}% + \pgfpointtaxicabnorm\pgf@xa + \ifdim\pgf@xa<0.1pt\relax % edge case: first three points are equal - \pgfpointdiff{#1}{#4} + \pgf@process{\pgfpointdiff{#1}{#4}}% \fi \fi - \pgfpointnormalised{} - \pgfstorepoint\pgf@tmp@tang@i - \pgfpointdiff{#3}{#4} - \pgfmathparse{abs(\pgf@x) + abs(\pgf@y)} - \ifdim\pgfmathresult pt<0.1pt\relax - \pgfpointdiff{#2}{#4} - \pgfmathparse{abs(\pgf@x) + abs(\pgf@y)} - \ifdim\pgfmathresult pt<0.1pt\relax - \pgfpointdiff{#1}{#4} + \pgfextract@process\pgf@tmp@tang@i{% + \pgfpointnormalised{}% + % \pgfpointnormalised stores the angle of the vector in \pgf@tmp + \global\let\pgf@nfold@tmp\pgf@tmp% + }% + \let\pgf@tmp@angle@i\pgf@nfold@tmp% + \pgf@process{\pgfpointdiff{#3}{#4}}% + \pgfpointtaxicabnorm\pgf@xa + \ifdim\pgf@xa<0.1pt\relax + \pgf@process{\pgfpointdiff{#2}{#4}}% + \pgfpointtaxicabnorm\pgf@xa + \ifdim\pgf@xa<0.1pt\relax + \pgf@process{\pgfpointdiff{#1}{#4}}% \fi \fi - \pgfpointnormalised{} - \pgfstorepoint\pgf@tmp@tang@ii + \pgfextract@process\pgf@tmp@tang@ii{\pgfpointnormalised{}\global\let\pgf@nfold@tmp\pgf@tmp}% + \let\pgf@tmp@angle@ii\pgf@nfold@tmp% } @@ -145,73 +169,89 @@ % Offsetting a simple section % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -\newcommand{\pgf@offset@bezier@segment}[5]{% - % TODO would it make sense to use \pgf@process here? +\def\pgf@offset@bezier@segment#1#2#3#4#5{% % normalise tangents and normals; this avoids overflow issues later, and we need % the normal vector to be of length 1 anyway - \pgf@offset@compute@tangents{#1}{#2}{#3}{#4} + \pgf@offset@compute@tangents{#1}{#2}{#3}{#4}% % offset A1 % compute the normal \pgf@tmp@tang@i - \pgf@xa=\pgf@x - \pgf@x=-\pgf@y - \pgf@y=\pgf@xa - \pgfstorepoint\pgf@tmp@normal@i - % Leaving this parameter empty amounts to working directly on the register - \pgfpointadd{\pgfpointscale{#5}{}}{#1} - \pgfstorepoint{\pgf@bezier@offset@i} + \edef\pgf@tmp@normal@i{\noexpand\pgfqpoint{-\the\pgf@y}{\the\pgf@x}}% + \pgfextract@process\pgf@bezier@offset@i + {\pgfpointadd{\pgfpointscale{#5}{\pgf@tmp@normal@i}}{#1}}% % offset A4 \pgf@tmp@tang@ii - \pgf@xa=\pgf@x - \pgf@x=-\pgf@y - \pgf@y=\pgf@xa - \pgfstorepoint\pgf@tmp@normal@ii - \pgfpointadd{\pgfpointscale{#5}{}}{#4} - \pgfstorepoint\pgf@bezier@offset@iv + \edef\pgf@tmp@normal@ii{\noexpand\pgfqpoint{-\the\pgf@y}{\the\pgf@x}}% + \pgfextract@process\pgf@bezier@offset@iv + {\pgfpointadd{\pgfpointscale{#5}{\pgf@tmp@normal@ii}}{#4}}% % now compute A'_2 and A'_3 - \pgfpointdiff{#1}{#4} - \pgfmathsetmacro{\pgf@tmp@secantlen}{veclen(\pgf@x,\pgf@y)} + \pgf@process{\pgfpointdiff{#1}{#4}}% + \pgfmathveclen@{\pgf@sys@tonumber\pgf@x}{\pgf@sys@tonumber\pgf@y}% + \let\pgf@tmp@secantlen\pgfmathresult \ifdim\pgf@tmp@secantlen pt<0.1pt\relax % Edge case: Either the curve is degenerate to a point or it is not simple. % Either way we offset A1 and A4, and preserve the vectors A1A2 and A3A4. - \pgfwarning{pgf-offset: first and last point are too close, expect glitches} - \pgfpointadd{\pgf@bezier@offset@i}{\pgfpointdiff{#1}{#2}} - \pgfstorepoint\pgf@bezier@offset@ii - \pgfpointadd{\pgf@bezier@offset@iv}{\pgfpointdiff{#4}{#3}} - \pgfstorepoint\pgf@bezier@offset@iii + \pgfutil@packagewarning{tikz-nfold}{first and last point are too close, expect glitches}% + \pgfextract@process\pgf@bezier@offset@ii + {\pgfpointadd{\pgf@bezier@offset@i}{\pgfpointdiff{#1}{#2}}}% + \pgfextract@process\pgf@bezier@offset@iii + {\pgfpointadd{\pgf@bezier@offset@iv}{\pgfpointdiff{#4}{#3}}}% \else - \pgfpointnormalised{} - \pgfstorepoint\pgf@tmp@secant - \pgfcrossdot{}{\pgf@tmp@tang@ii} - \ifdim\pgf@tmp@dot<.5pt\relax% + \pgfextract@process\pgf@tmp@secant{\pgfpointnormalised{}}% + \pgfmathcrossdot{}{\pgf@tmp@tang@ii}% + \ifdim\pgf@tmp@dot pt<.5pt\relax% % this can only happen in non-simple curves - \pgfwarning{pgf-offset: cosine of \pgf@tmp@dot\space clamped to 0.5 in non-simple segment}% - \def\pgf@tmp@dot{.5pt}% + \pgfutil@packagewarning{tikz-nfold}{cosine of \pgf@tmp@dot\space clamped to 0.5 in non-simple segment}% + \def\pgf@tmp@dot{.5}% \fi% - \pgfmathsetmacro{\pgf@tmp@tanbeta}{\pgf@tmp@cross/\pgf@tmp@dot}% - \pgfcrossdot{\pgf@tmp@secant}{\pgfpointnormalised{\pgfpointdiff{#1}{#2}}} + \pgfmathdivide@{\pgf@tmp@cross}{\pgf@tmp@dot}% + \let\pgf@tmp@tanbeta\pgfmathresult + \pgfmathcrossdot{\pgf@tmp@secant}{\pgfpointnormalised{\pgfpointdiff{#1}{#2}}} % There are cases where we want #5/secantlen to be quite large, so we should not clamp the value here - \pgfmathparse{1 + #5/\pgf@tmp@secantlen*(\pgf@tmp@cross - \pgf@tmp@dot*\pgf@tmp@tanbeta)} - \pgfpointadd% - {\pgf@bezier@offset@i}% - {\pgfpointscale{\pgfmathresult pt}{\pgfpointdiff{#1}{#2}}}% - \pgfstorepoint\pgf@bezier@offset@ii + % \pgfmathparse{1 + #5/\pgf@tmp@secantlen*(\pgf@tmp@cross - \pgf@tmp@dot*\pgf@tmp@tanbeta)}% + \pgfmath@offset@calculate@scale{\pgf@tmp@secantlen}{\pgf@tmp@cross}{\pgf@tmp@dot}{\pgf@tmp@tanbeta}{#5}% + \pgfextract@process\pgf@bezier@offset@ii{% + \pgfpointadd + {\pgf@bezier@offset@i} + {\pgfqpointscale{\pgfmathresult}{\pgfpointdiff{#1}{#2}}}% + }% % third control point - \pgfcrossdot{\pgf@tmp@secant}{\pgf@tmp@tang@i} - \ifdim\pgf@tmp@dot<.5pt\relax% - \pgfwarning{pgf-offset: cosine of \pgf@tmp@dot\space clamped to 0.5 in non-simple segment}% - \def\pgf@tmp@dot{.5pt}% - \fi% - \pgfmathsetmacro{\pgf@tmp@tanbeta}{\pgf@tmp@cross/\pgf@tmp@dot}% - \pgfcrossdot{\pgf@tmp@secant}{\pgfpointnormalised{\pgfpointdiff{#4}{#3}}}% - \pgfmathparse{1 + #5/\pgf@tmp@secantlen*(\pgf@tmp@cross - \pgf@tmp@dot*\pgf@tmp@tanbeta)}% - \pgfpointadd% - {\pgf@bezier@offset@iv}% - {\pgfpointscale{\pgfmathresult pt}{\pgfpointdiff{#4}{#3}}}% - \pgfstorepoint\pgf@bezier@offset@iii% + \pgfmathcrossdot{\pgf@tmp@secant}{\pgf@tmp@tang@i}% + \ifdim\pgf@tmp@dot pt<.5pt\relax + \pgfutil@packagewarning{tikz-nfold}{cosine of \pgf@tmp@dot\space clamped to 0.5 in non-simple segment}% + \def\pgf@tmp@dot{.5}% + \fi + \pgfmathdivide@{\pgf@tmp@cross}{\pgf@tmp@dot}% + \let\pgf@tmp@tanbeta\pgfmathresult + \pgfmathcrossdot{\pgf@tmp@secant}{\pgfpointnormalised{\pgfpointdiff{#4}{#3}}}% + % \pgfmathparse{1 + #5/\pgf@tmp@secantlen*(\pgf@tmp@cross - \pgf@tmp@dot*\pgf@tmp@tanbeta)}% + \pgfmath@offset@calculate@scale{\pgf@tmp@secantlen}{\pgf@tmp@cross}{\pgf@tmp@dot}{\pgf@tmp@tanbeta}{#5}% + \pgfextract@process\pgf@bezier@offset@iii{% + \pgfpointadd + {\pgf@bezier@offset@iv} + {\pgfqpointscale{\pgfmathresult}{\pgfpointdiff{#4}{#3}}}% + }% \fi } +% calculates 1+#5/#1*(#2-#3*#4) +% #1 = secantlen +% #2 = cross +% #3 = dot +% #4 = tanbeta +% #5 = #5 (offset) +\def\pgfmath@offset@calculate@scale#1#2#3#4#5{% + \begingroup + \pgfmathmultiply@{#3}{#4}% + \pgfmathsubtract@{#2}{\pgfmathresult}% + \let\pgfmath@temp\pgfmathresult + \pgfmathreciprocal@{#1}% + \pgfmathmultiply@{\pgfmathresult}{\pgfmath@temp}% + \pgfmathmultiply{\pgfmathresult}{#5}% + \pgfmathadd@{\pgfmathresult}{1}% + \pgfmath@smuggleone\pgfmathresult + \endgroup +} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Subdividing and offsetting % @@ -225,35 +265,42 @@ % % Subdivides a Bezier curve into "simple" segments (according to the definition below), % offsets the segments, and draws them. Because offsetting also involves relocating -% the starting points, these macros come in two variants: with and without a \pgfmoveto{} +% the starting points, these macros come in two variants: with and without a \pgfpathmoveto{} % to the new starting point. % % Interface: % #1-#4: control points of the whole Bezier curve % #5: offset -\newcommand{\pgfoffsetcurve}[5]{% - \pgf@subdivideandoffsetcurve{#1}{#2}{#3}{#4}{#5}{\pgf@offset@max@recursion}{0}{\pgf@nfold@callback@move}% +% \def\pgfoffsetcurve#1#2#3#4#5{% +% \pgf@subdivideandoffsetcurve{#1}{#2}{#3}{#4}{#5}{\pgf@offset@max@recursion}{0}{\pgf@nfold@callback@move}% +% } +% \def\pgfoffsetcurvenomove#1#2#3#4#5{% +% \pgf@subdivideandoffsetcurve{#1}{#2}{#3}{#4}{#5}{\pgf@offset@max@recursion}{0}{\pgf@nfold@callback@nomove}% +% } + +\def\pgfoffsetcurve#1#2#3#4#5{% + \pgfoffsetcurvecallback{#1}{#2}{#3}{#4}{#5}{\pgf@nfold@callback@move}% } -\newcommand{\pgfoffsetcurvenomove}[5]{% - \pgf@subdivideandoffsetcurve{#1}{#2}{#3}{#4}{#5}{\pgf@offset@max@recursion}{0}{\pgf@nfold@callback@nomove}% +\def\pgfoffsetcurvenomove#1#2#3#4#5{% + \pgfoffsetcurvecallback{#1}{#2}{#3}{#4}{#5}{\pgf@nfold@callback@nomove}% } % Arguments: % #1-#4: control points of the segment % #5: =0 if this is the first segment of the curve, =1 otherwise % (checking for #5=0 allows us to draw the curve without interruptions) -\newcommand{\pgf@nfold@callback@move}[5]{% +\def\pgf@nfold@callback@move#1#2#3#4#5{% \ifnum#5=0\relax\pgfpathmoveto{#1}\fi% \pgfpathcurveto{#2}{#3}{#4}% } % this version never does a moveto at the start. Useful for drawing a path consisting of % multiple Bezier curves. -\newcommand{\pgf@nfold@callback@nomove}[5]{\pgfpathcurveto{#2}{#3}{#4}} +\def\pgf@nfold@callback@nomove#1#2#3#4#5{\pgfpathcurveto{#2}{#3}{#4}} % Like the previous macro, but with a custom callback macro for each segment instead of % executing \drawsegment as defined above. See \drawsegment for the arguments. -\newcommand{\pgfoffsetcurvecallback}[6]{% +\def\pgfoffsetcurvecallback#1#2#3#4#5#6{% \pgf@subdivideandoffsetcurve{#1}{#2}{#3}{#4}{#5}{\pgf@offset@max@recursion}{0}{#6}% } @@ -265,66 +312,73 @@ % #7: =0 if this is the start of the curve, =1 otherwise; % #8: callback for output (see above) \newif\ifpgf@offset@subdivide -\newcommand{\pgf@subdivideandoffsetcurve}[8]{% +\def\pgf@subdivideandoffsetcurve#1#2#3#4#5#6#7#8{% % we need a group to avoid overwriting variables in recursive calls \begingroup% - \pgf@offset@subdividefalse% - \c@pgf@counta=#6% - \advance\c@pgf@counta by-1% - \pgf@process{#1}\pgfstorepoint{\pgf@ctrl@i}% - \pgf@process{#2}\pgfstorepoint{\pgf@ctrl@ii}% - \pgf@process{#3}\pgfstorepoint{\pgf@ctrl@iii}% - \pgf@process{#4}\pgfstorepoint{\pgf@ctrl@iv}% - % Use the non-degenerate tangents for the simplicity check - \pgf@offset@compute@tangents{\pgf@ctrl@i}{\pgf@ctrl@ii}{\pgf@ctrl@iii}{\pgf@ctrl@iv}% - \pgfpointdiff{\pgf@ctrl@i}{\pgf@ctrl@iv}\pgfstorepoint{\pgf@itoiv}% - \pgfcrossproduct{\pgf@itoiv}{\pgf@tmp@tang@i}% - \edef\firstcross{\pgfmathresult}% - \pgfcrossproduct{\pgf@itoiv}{\pgf@tmp@tang@ii}% - % First simplicity check: Are A2 and A3 on the same side of the A1-A4 line? - % -> compute the sign of the cross products, use the sign function to avoid overflows - \pgfmathparse{sign(\firstcross)*sign(\pgfmathresult)}% - \ifdim\pgfmathresult pt>0pt\relax% - \pgf@offset@subdividetrue% - \else% - % Second simplicity check: How large is the angle between the tangents in A1 and A4? - \pgfdotproduct{\pgf@tmp@tang@i}{\pgf@tmp@tang@ii}% - \ifdim\pgfmathresult pt<.5pt\relax% + \pgf@offset@subdividefalse% + \c@pgf@counta=#6\relax + \advance\c@pgf@counta by -1 + \pgfextract@process\pgf@ctrl@i{#1}% + \pgfextract@process\pgf@ctrl@ii{#2}% + \pgfextract@process\pgf@ctrl@iii{#3}% + \pgfextract@process\pgf@ctrl@iv{#4}% + % Use the non-degenerate tangents for the simplicity check + \pgf@offset@compute@tangents{\pgf@ctrl@i}{\pgf@ctrl@ii}{\pgf@ctrl@iii}{\pgf@ctrl@iv}% + \pgfextract@process\pgf@itoiv{\pgfpointdiff{\pgf@ctrl@i}{\pgf@ctrl@iv}}% + \pgfmathcrossproduct{\pgf@itoiv}{\pgf@tmp@tang@i}% + \let\firstcross\pgfmathresult + \pgfmathcrossproduct{\pgf@itoiv}{\pgf@tmp@tang@ii}% + % First simplicity check: Are A2 and A3 on the same side of the A1-A4 line? + % -> compute the sign of the cross products, use the sign function to avoid overflows + % just give it a pass if one of them is zero, hence 2 and 3 at the end + \ifnum + \ifdim \firstcross pt<0pt -1\else\ifdim \firstcross pt>0pt 1\else 2\fi\fi + =\ifdim\pgfmathresult pt<0pt -1\else\ifdim\pgfmathresult pt>0pt 1\else 3\fi\fi + \relax % the \relax is important! \pgf@offset@subdividetrue% - \else - % Third simplicity check: Put a limit on the lengths of the i-ii and iii-iv vectors combined - \pgf@itoiv - \pgfmathsetmacro{\pgf@tmp@len@i@iv}{veclen(\pgf@x,\pgf@y)} - \pgfpointdiff{\pgf@ctrl@i}{\pgf@ctrl@ii} - \pgf@xb=\pgf@x\pgf@yb=\pgf@y - \pgfpointdiff{\pgf@ctrl@iii}{\pgf@ctrl@iv} - \pgfmathparse{\pgf@tmp@len@i@iv < veclen(\pgf@xb,\pgf@yb) + veclen(\pgf@x,\pgf@y)} - \ifnum\pgfmathresult=1\relax + \else% + % Second simplicity check: How large is the angle between the tangents in A1 and A4? + \pgfmathdotproduct{\pgf@tmp@tang@i}{\pgf@tmp@tang@ii}% + \ifdim\pgfmathresult pt<.5pt\relax% \pgf@offset@subdividetrue% - \fi + \else + % Third simplicity check: Put a limit on the lengths of the i-ii and iii-iv vectors combined + \pgf@itoiv + \pgfmathveclen@{\pgf@sys@tonumber\pgf@x}{\pgf@sys@tonumber\pgf@y}% + \pgf@xa=\pgfmathresult pt + \pgf@process{\pgfpointdiff{\pgf@ctrl@i}{\pgf@ctrl@ii}}% + \pgfmathveclen@{\pgf@sys@tonumber\pgf@x}{\pgf@sys@tonumber\pgf@y}% + \pgf@xb=\pgfmathresult pt + \pgf@process{\pgfpointdiff{\pgf@ctrl@iii}{\pgf@ctrl@iv}}% + \pgfmathveclen@{\pgf@sys@tonumber\pgf@x}{\pgf@sys@tonumber\pgf@y}% + \advance\pgf@xb by \pgfmathresult pt + % \veclen(itoiv) < veclen(ii-i) + veclen(iv-iii) + \ifdim\pgf@xa<\pgf@xb + \pgf@offset@subdividetrue + \fi + \fi% \fi% - \fi% - \ifpgf@offset@subdivide% - \ifnum\c@pgf@counta<0% - % We hit the recursion limit but the segment is not simple - \pgfwarning{pgf-offset: Recursion limit reached, glitches may occur. % - Consider increasing \string\pgf@offset@max@recursion}% - % Try to offset the curve anyway. The result will not be precise, - % but the code is sufficiently robust to not crash + \ifpgf@offset@subdivide% + \ifnum\c@pgf@counta<0% + % We hit the recursion limit but the segment is not simple + \pgfutil@packagewarning{tikz-nfold}{Recursion limit reached, glitches may occur. % + Consider increasing \string\pgf@offset@max@recursion}% + % Try to offset the curve anyway. The result will not be precise, + % but the code is sufficiently robust to not crash + \pgf@offset@bezier@segment{\pgf@ctrl@i}{\pgf@ctrl@ii}{\pgf@ctrl@iii}{\pgf@ctrl@iv}{#5}% + #8{\pgf@bezier@offset@i}{\pgf@bezier@offset@ii}{\pgf@bezier@offset@iii}{\pgf@bezier@offset@iv}{#7}% + \else + % split the non-simple segment and execute recursive calls + \pgf@splitbezier{.5}{\pgf@ctrl@i}{\pgf@ctrl@ii}{\pgf@ctrl@iii}{\pgf@ctrl@iv}% + % pass on the "start of the curve flag" only to the first term + \pgf@subdivideandoffsetcurve{\pgf@splitbezier@i@i}{\pgf@splitbezier@i@ii}{\pgf@splitbezier@i@iii}{\pgf@splitbezier@i@iv}{#5}{\c@pgf@counta}{#7}{#8}% + \pgf@subdivideandoffsetcurve{\pgf@splitbezier@ii@i}{\pgf@splitbezier@ii@ii}{\pgf@splitbezier@ii@iii}{\pgf@splitbezier@ii@iv}{#5}{\c@pgf@counta}{1}{#8}% + \fi% + \else% + % curve is simple \pgf@offset@bezier@segment{\pgf@ctrl@i}{\pgf@ctrl@ii}{\pgf@ctrl@iii}{\pgf@ctrl@iv}{#5}% #8{\pgf@bezier@offset@i}{\pgf@bezier@offset@ii}{\pgf@bezier@offset@iii}{\pgf@bezier@offset@iv}{#7}% - \else - % split the non-simple segment and execute recursive calls - \pgf@splitbezier{.5}{\pgf@ctrl@i}{\pgf@ctrl@ii}{\pgf@ctrl@iii}{\pgf@ctrl@iv}% - % pass on the "start of the curve flag" only to the first term - \pgf@subdivideandoffsetcurve{\pgf@splitbezier@i@i}{\pgf@splitbezier@i@ii}{\pgf@splitbezier@i@iii}{\pgf@splitbezier@i@iv}{#5}{\c@pgf@counta}{#7}{#8}% - \pgf@subdivideandoffsetcurve{\pgf@splitbezier@ii@i}{\pgf@splitbezier@ii@ii}{\pgf@splitbezier@ii@iii}{\pgf@splitbezier@ii@iv}{#5}{\c@pgf@counta}{1}{#8}% \fi% - \else% - % curve is simple - \pgf@offset@bezier@segment{\pgf@ctrl@i}{\pgf@ctrl@ii}{\pgf@ctrl@iii}{\pgf@ctrl@iv}{#5}% - #8{\pgf@bezier@offset@i}{\pgf@bezier@offset@ii}{\pgf@bezier@offset@iii}{\pgf@bezier@offset@iv}{#7}% - \fi% \endgroup% } @@ -336,16 +390,16 @@ % For convenience we also provide macros that offset straight lines. These also come in two variants % similar to the macros for curves. % -\newcommand{\pgfoffsetline}[3]{ - \pgfpointscale{#3}{\pgfpointnormalised{\pgfpointdiff{#1}{#2}}} +\def\pgfoffsetline#1#2#3{% + \pgfpointscale{#3}{\pgfpointnormalised{\pgfpointdiff{#1}{#2}}}% \pgf@xc=-\pgf@y \pgf@yc=\pgf@x - \pgfpathmoveto{\pgfpointadd{#1}{\pgfqpoint{\pgf@xc}{\pgf@yc}}} - \pgfpathlineto{\pgfpointadd{#2}{\pgfqpoint{\pgf@xc}{\pgf@yc}}} + \pgfpathmoveto{\pgfpointadd{#1}{\pgfqpoint{\pgf@xc}{\pgf@yc}}}% + \pgfpathlineto{\pgfpointadd{#2}{\pgfqpoint{\pgf@xc}{\pgf@yc}}}% } -\newcommand{\pgfoffsetlinenomove}[3]{ - \pgfpointscale{#3}{\pgfpointnormalised{\pgfpointdiff{#1}{#2}}} +\def\pgfoffsetlinenomove#1#2#3{% + \pgfpointscale{#3}{\pgfpointnormalised{\pgfpointdiff{#1}{#2}}}% \pgf@xc=-\pgf@y \pgf@yc=\pgf@x - \pgfpathlineto{\pgfpointadd{#2}{\pgfqpoint{\pgf@xc}{\pgf@yc}}} + \pgfpathlineto{\pgfpointadd{#2}{\pgfqpoint{\pgf@xc}{\pgf@yc}}}% } diff --git a/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.pdf b/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.pdf Binary files differindex e57496affc..706890046b 100644 --- a/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.pdf +++ b/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.pdf diff --git a/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.tex b/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.tex index 18bd047e7b..45cdf906c7 100644 --- a/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.tex +++ b/graphics/pgf/contrib/tikz-nfold/tikz-nfold-doc.tex @@ -32,6 +32,8 @@ \usepackage{amsmath} \usepackage{amsthm} \usepackage[capitalize]{cleveref} +\usepackage{datetime} +\usepackage{enumitem} \theoremstyle{definition} \newtheorem{definition}{Definition}[section] @@ -41,26 +43,34 @@ \newcommand{\nfold}{\texttt{nfold}} \newcommand{\tikznfold}{\texttt{/tikz/nfold}} -\pgfkeys{/tkzexample/every tkzexample/.style={code=violet!15, graphic=orange!20, very small}} +\pgfkeys{/tkzexample/every tkzexample/.style={ + code=violet!15, graphic=orange!20, very small,above skip={\vskip1em},below skip={\vskip1em} + } +} \newcommand{\tv}{\vec{t}} \newcommand{\uv}{\vec{u}} \newcommand{\vv}{\vec{v}} +% to be used in pre=... to stop the proparation of warnings outside of tkzexample environments +\makeatletter +\def\disablewarnings{\def\pgfutil@packagewarning##1##2{}} +\makeatother + \begin{document} \title{The \textsf{tikz-nfold} package} \author{Jonathan Schulz} -\date{March 2023} +\date{\monthname{} \the\year} \maketitle \begin{abstract} - This package provides an alternative to TikZ' \verb|/tikz/double| option, avoiding some shortcomings of the original approach. It also provides an option to draw triple, quadruple, and n-fold paths. + This package provides an alternative to TikZ' \verb|/tikz/double| option, avoiding some shortcomings of the original approach. Further features include options to draw triple, quadruple, and n-fold paths as well as macros to offset arbitrary paths. \end{abstract} \section*{Compatibility} -This package has been tested with \texttt{pdflatex}, \texttt{lualatex} and \texttt{xelatex}. Support for plain \TeX{} could, in principle, be implemented in the future as well. +This package has been tested with \texttt{pdflatex}, \texttt{lualatex}, \texttt{xelatex}, and plain \texttt{pdftex}. \section{Quick start} @@ -68,8 +78,7 @@ Add \begin{verbatim} \usetikzlibrary{nfold} \end{verbatim} -to your preamble. Now you can add the style \tikznfold{} to any path that uses \tikzdouble: - +to your preamble. Now you can add \tikznfold{} to any path that uses \tikzdouble. Be sure to specify \verb|/tikz/double distance| before \tikznfold, as otherwise the latter will not be applied. \begin{tkzexample}[latex=4cm] \begin{tikzpicture} \draw[double distance=3pt] @@ -80,8 +89,7 @@ to your preamble. Now you can add the style \tikznfold{} to any path that uses \ \node[right] at (0,1) {nfold:}; \end{tikzpicture} \end{tkzexample} -While it appears that adding \nfold{} does not do much here, it avoids some rendering issues of \tikzdouble, hence I recommend using it in most cases (see \cref{subsec:doubleIssues} for details). - +While it appears that adding \nfold{} does not do much here, it avoids some rendering issues of \tikzdouble, hence I recommend using it in most cases (see \cref{sec:doubleIssues} for details). Specify a number for n-fold lines: \begin{tkzexample}[latex=4cm] \begin{tikzpicture} @@ -89,17 +97,27 @@ Specify a number for n-fold lines: (0,0) to[out=0, in=180] (3,1) -- (3,0); \end{tikzpicture} \end{tkzexample} - -The arrow tips \texttt{Implies} and \texttt{Bar} are supported (the latter can also be aliased by \texttt{|}): -\begin{tkzexample}[, latex=4cm] +All arrow tips are supported, and there is special treatment for the \texttt{Implies} tip: +\begin{tkzexample}[latex=4cm] \begin{tikzpicture} - \draw[double distance=7pt, nfold=5, arrows=Bar-Implies] + \draw[double distance=7pt, nfold=5, + arrows={Bar[width=18pt]-Implies[red]}] (0,1.5) to[bend left] (3,1.5); \draw[double distance=7pt, nfold=5, arrows=Implies-Implies] (0,0) to[bend left] (3,0); \end{tikzpicture} \end{tkzexample} - +Use \texttt{/tikz/scaling nfold} to preserve the distance between component lines instead of the overall width of the arrow: +\begin{tkzexample}[latex=4cm,pre={\usetikzlibrary{intersections}}] +\begin{tikzpicture} + \draw[double equal sign distance, nfold, + arrows=-Implies] (0,.75) -- (3,.75); + \draw[double equal sign distance, scaling nfold=4, + arrows=-Implies] (0,0) -- (3,0); + \draw[double equal sign distance, scaling nfold=6, + arrows=-Implies] (0,-1) -- (3,-1); +\end{tikzpicture} +\end{tkzexample} Different line joins are supported: \begin{tkzexample}[latex=2cm] \begin{tikzpicture}[line join=bevel] @@ -111,18 +129,19 @@ Different line joins are supported: (0,0) -- (1, 0) -- (1,.5); \end{tikzpicture} \end{tkzexample} - -There is also support for \texttt{tikz-cd}: +There is also support for \texttt{tikz-cd} with custom label positions for \texttt{scaling nfold}: \begin{tkzexample}[pre={\usetikzlibrary{cd}}, latex=3cm] \begin{tikzcd} - a \ar[r, Rightarrow, bend right, nfold=3] & - b \ar[d, Mapsto, nfold=3, "\alpha", "\beta"'] \\ - c \ar[r, Mapsfrom, double distance=4pt, nfold=4] & + a \ar[r, Mapsto, bend left, scaling nfold=3] & + b \ar[d, Rightarrow, nfold, "\alpha", "\beta"'] \\ + c \ar[r, Mapsfrom, scaling nfold=4, "\gamma" near end] & d \end{tikzcd} \end{tkzexample} + \section{Comparison to \tikzdouble} +\label{sec:doubleIssues} This package does \emph{not} aim to supersede \tikzdouble, as both the original and the \nfold{} approach have their own strengths and weaknesses. The main difference is that \tikzdouble{} achieves its goal by drawing the original path twice, once very thick with the foreground colour and then slightly less thick with the background colour. By contrast, \nfold{} offsets the path: \begin{equation} @@ -143,12 +162,8 @@ This package does \emph{not} aim to supersede \tikzdouble, as both the original \draw[double distance=5pt, nfold] (4,0) -- (5,0); \end{tikzpicture} \end{equation} - - -\subsection{Issues with \tikzdouble} -\label{subsec:doubleIssues} While the approach of \tikzdouble{} is very robust and efficient, it does have a few pitfalls: -\begin{itemize} +\begin{itemize}[beginpenalty=10000] \item Different types of visual glitches can occur in PDF renderers: \begin{itemize} \item One common issue is that the white foreground piece completely covers the black background piece at certain zoom levels, leading to the top or bottom part of the doubled path missing (depending on your PDF viewer and zoom level, this issue might be visible in \cref{eq:compareApproaches}). @@ -179,13 +194,23 @@ While the approach of \tikzdouble{} is very robust and efficient, it does have a \end{tkzexample} \item Triple and n-fold paths are not supported (although this could be implemented in principle). \end{itemize} +However, there are still situations where \tikznfold{} struggles and \tikzdouble{} is the only viable option, which will be discussed in the next section. -\subsection{Issues with \tikznfold} +\section{Known issues} + +This package is by no means perfect, and even if it were, there would still be some cases where the approach of \tikzdouble{} is better suited. The known issues are roughly sorted into those that can be fixed in principle and the fundamental limitations of this approach. If you find any bugs not listed here, please report them \href{https://github.com/jonschz/tikz-nfold/issues/}{here}. + +\subsection{Fixable / wish list} +\begin{itemize} + \item \nfold{} is significantly slower than \tikzdouble. Part of the reason is that the construction is far more complex, but the code is also far from fully optimised. + \item Closing paths (i.e.\ using \texttt{-- cycle}) is not yet fully supported: The output will not be correct if the angle between the first and last segment differs from zero. + \item Some rare cases of curves are not offset correctly. The reasons for that are discussed below in \cref{subsec:subdivisionUsed}. Usually, slightly changing the control points or values of the curve will fix the problem. If you find any, please open an issue. +\end{itemize} -This package is far from perfect, and even if it were, there would still be some cases where the approach of \tikzdouble{} is better suited. Here are some shortcomings of \nfold: +\subsection{Impossible or very hard to fix} \begin{itemize} \item \nfold{} struggles with high curvatures and wide paths: Let $\kappa(t)$ be the curvature of the path in a given point, and let $\texttt{double distance} = \alpha$. If $\kappa(t) > \frac{2}{\alpha}$ (i.e. the radius of the osculating circle is smaller than half the width of the path) for some $0 \leq t \leq 1$, the output of \nfold{} will not be correct: -\begin{tkzexample}[latex=3.5cm] +\begin{tkzexample}[latex=4.25cm] \begin{tikzpicture} \draw[double distance=5pt, line width=1pt] (0,2) .. controls (4,2) and (0,3) .. (3,2.5); @@ -195,30 +220,14 @@ This package is far from perfect, and even if it were, there would still be some \node[right] at (0,1) {nfold:}; \end{tikzpicture} \end{tkzexample} - Some, but not all of these cases raise warnings (this feature is on the wish list). - \item Some rare cases of curves are not offset correctly. The reasons for that are discussed below in \cref{subsec:subdivisionUsed}. Usually, slightly changing the control points or values of the curve will fix the problem. - \item Closing paths (i.e.\ using \texttt{-- cycle}) is not yet supported (this feature is on the wish list). - \item \nfold{} is significantly slower than \tikzdouble. Part of the reason is that the construction is far more complex, other reasons can be fixed in principle. Specifically, the use of the \texttt{decorations} library is rather inefficient for this purpose. - \item While I did my best trying to break \nfold{} with as many of TikZ' options as possible, there are most definitely some problematic options I have not tested. If you find any bugs, please report them \href{https://github.com/jonschz/tikz-nfold/issues/}{here}. -\end{itemize} - - -\section{Known issues} - -\subsection{Fixable / wish list} - -\begin{itemize} - \item Closing paths (e.g.\ \texttt{-- cycle}) is not yet properly supported. - \item The arrow detection is still quite rough and likely has some bugs. One known issue is that you cannot provide parameters to the arrow tips (e.g. \texttt{Implies[red]}). - \item Similarly, redefining arrows does not have the desired effect (usually, it has no effect at all). - \item It would not be very hard to check for too much curvature in the offsetting algorithm and throw warnings in these cases. - \item Migrating away from the \texttt{decorations} library and integrating this algorithm more tightly with the rendering pipeline would be possible. I expect that doing so will significantly improve the performance and also fix most arrow-related issues. This idea is on the back burner for now, as it likely requires changes to the TikZ rendering pipeline. If the TikZ team shows interest in integrating this library, I will reconsider this. - \item Discontinuous paths with an arrow tip at the start are rendered differently in \tikzdouble{} and \nfold. As I do not really see a use case for such paths (and a rendering pipeline integration would likely fix it anyway), this issue is not a priority for now. -\end{itemize} - -\subsection{Impossible or very hard to fix} -\begin{itemize} - \item Correctly rendering paths with too much curvature is borderline impossible with this approach. This is one of the cases where using \tikzdouble{} is the only viable option. + Some, but not all of these cases raise warnings (this feature is on the wish list). This is one of the cases where using \tikzdouble{} is the only viable option. + \item Dashed paths with significant curvature will desynchronise: +\begin{tkzexample}[latex=3cm] +\begin{tikzpicture} + \draw[arrows=-Implies, double equal sign distance, dashed, + scaling nfold=4] (0,0) to[out=30, in=150] (2,0); +\end{tikzpicture} +\end{tkzexample} \item Curves of \nfold{} slightly deviate from the curves of \tikzdouble{} near joins with a non-zero angle: \begin{tkzexample}[latex=3.5cm] \begin{tikzpicture}[line width=1pt] @@ -231,7 +240,18 @@ This package is far from perfect, and even if it were, there would still be some \end{tikzpicture} \end{tkzexample} This cannot be fixed without extensive use of the \texttt{intersections} library, hurting the performance, and the result might still not look great for orders $\geq 3$. - \item changing joins in \verb|\pgfsys@beginscope| without an accompanying \TeX{} group may cause inconsistent behaviour in the joins. For example, + \item Very short \emph{curves} with large angles at the ends result in a glitched output: +\begin{tkzexample}[latex=3.75cm,pre=\disablewarnings] +\begin{tikzpicture} + \draw[black] (1,0) -- (3,0) -- (1.5,1.2) -- (3.8,0.85); + \draw[red, line width=1pt, double distance=.6cm, nfold] + (1,0) -- (3,0) -- (1.5, 1.2) -- (3.8,0.85); + \draw[blue, double distance=.6cm, nfold] (1,0) -- (3,0) + to[relative, out=1, in=179] (1.5, 1.2) -- (3.8,0.85); +\end{tikzpicture} +\end{tkzexample} + This issue has been fixed for \emph{straight lines} in version \texttt{0.1.0} (note how the red line is offset correctly), but it is much harder to fix for curves. + \item Changing joins in \verb|\pgfsys@beginscope| without an accompanying \TeX{} group may cause inconsistent behaviour in the joins: \begin{tkzexample}[latex=2cm] \makeatletter \begin{tikzpicture}[line join=miter, line width=2pt] @@ -242,27 +262,89 @@ This package is far from perfect, and even if it were, there would still be some \end{tikzpicture} \makeatother \end{tkzexample} - has \texttt{round} joins on the large path, but \texttt{miter} joins on the constituent paths. This problem does not occur with \verb|\pgfscope|. + This example has \texttt{round} joins on the large path but \texttt{miter} joins on the constituent paths. This problem does not occur with \verb|\pgfscope|. \end{itemize} + \section{The basic layer \texttt{pgf} commands} -This package also provides some basic layer commands for offsetting curves and straight lines. Use +\subsection{Offsetting curves} +This package provides some basic layer commands for offsetting curves and straight lines. Use \begin{verbatim} \usepgflibrary{bezieroffset} \end{verbatim} to only import the base layer library. The following commands are provided: \begin{itemize} - \item \verb|\pgfoffsetcurve|: This macro draws the parallel of a Bézier curve. It takes five parameters, the first four being the four control points of the Bézier curve (e.g.\ in the form of \verb|\pgfpoint{}{}|), the fifth parameter is the distance by which the curve should be offset. A negative value offsets the curve in the opposite direction. This macro begins with a \verb|\pgfpointmoveto| to the offset first control point. - \item \verb|\pgfoffsetcurvenomove|: The only difference to the previous macro is that this version does not move to the offset first control point. This is useful if one wants to offset an uninterrupted path consisting of several curves. The output will only be correct if the previous path segment ends on the offset first control point. - \item \verb|\pgfoffsetline|: This macro offsets a straight line. It takes two points and the distance as parameters. - \item \verb|\pgfoffsetlinenomove|: This macro is analogous to \verb|\pgfoffsetcurvenomove|. + \item \verb|\pgfoffsetcurve{pt1}{pt2}{pt3}{pt4}{distance}|\\% + This macro draws the parallel of a Bézier curve. The first four parameters are the control points of the Bézier curve (e.g.\ in the form of \verb|\pgfpoint{}{}|), the fifth parameter is the distance by which the curve should be offset. A negative value offsets the curve in the opposite direction. This macro begins with a \verb|\pgfpointmoveto| to the offset of \texttt{pt1}. + \item \verb|\pgfoffsetcurvenomove{pt1}{pt2}{pt3}{pt4}{distance}|\\% + The only difference to the previous macro is that this version does not move to the offset of \texttt{pt1}. This is useful if one wants to offset an uninterrupted path consisting of several curves. The output will only be correct if the previous \verb|\pgfpath...| call ends on the offset of \texttt{pt1}. + \item \verb|\pgfoffsetline{pt1}{pt2}{distance}|\\% + This macro offsets a straight line. It takes two points and the distance as parameters, and starts by moving to the offset of the first point. + \item \verb|\pgfoffsetlinenomove{pt1}{pt2}{distance}|\\% + This macro is analogous to \verb|\pgfoffsetcurvenomove|. +\end{itemize} + +\subsection{Offsetting paths} +The following macros are part of the TikZ library \nfold{} and offset an entire soft path. +\begin{itemize} + \item \verb|\pgfoffsetpath{softpath}{distance}|\\% + This macro offsets \texttt{softpath} by \texttt{distance}. The latter may be negative. + \item \verb|\pgfoffsetpathfraction{softpath}{hwidth}{fraction}|\\% + This macro offsets \texttt{softpath} by \texttt{fraction*hwidth}. Note that this is \emph{not} equivalent to the previous macro with \texttt{length=fraction*hwidth} because the joins are treated differently, as can be seen in the examples below. Further note that \texttt{hwidth} must not be negative, and that \texttt{fraction=0} does \emph{not} reproduce the input path. + \item \verb|\pgfoffsetpathqfraction{softpath}{hwidth}{fraction}|\\% + This macro is a quicker version of the previous macro does not parse the input values using the \texttt{pgfmath}-engine. + \item \verb|\pgfoffsetpathindex{softpath}{width}{i}{n}|\\% + In this convenience method, \texttt{i} and \texttt{n} are integers with $1 \leq i \leq n$. It calls the previous macro with \texttt{fraction=-1.0} for \texttt{i=1} and with \texttt{fraction=1.0} for \texttt{i=n} and is thus capable of reproducing the output of \texttt{/tikz/nfold=n} (albeit in a less efficient way). \end{itemize} +In the following example we see how \verb|\pgfoffsetpath{..}{0pt}| reproduces the input path (rendered in black) and how \verb|\pgfoffsetpathfraction{..}{8pt}{0}| differs. +\begin{tkzexample}[latex=4cm] +\begin{tikzpicture}[line join=bevel] + \path[save path=\savedpath] (0,0) -- (1,0) + to[out=0, in=-80] (1,3) -- (3,2); + \draw[color=lightgray,line width=16pt,use path=\savedpath]; + \pgfoffsetpathfraction{\savedpath}{8pt}{0} + \pgfsetlinewidth{1pt} \color{red} \pgfusepathqstroke + \pgfoffsetpath{\savedpath}{8pt} + \color{blue} \pgfusepathqstroke + \pgfoffsetpath{\savedpath}{-8pt} + \color{green} \pgfusepathqstroke + \pgfoffsetpath{\savedpath}{0pt} + \pgfsetlinewidth{.4pt} \color{black} \pgfusepathqstroke +\end{tikzpicture} +\end{tkzexample} +Here we see how the commands can be used to customise $n$-fold paths: +\begin{tkzexample}[latex=4cm] +\begin{tikzpicture} + \path[save path=\mypath] (0,0) -- (2,0) arc(-90:90:1) + to[out=180, in=0] (0,1) -- (0,2); + \foreach \mycolor [count=\i] in {red,green,blue,violet} + \pgfoffsetpathindex{\mypath}{6pt}{\i}{4} + \color{\mycolor} \pgfusepathqstroke; +\end{tikzpicture} +\end{tkzexample} +\newpage +\section{Version history} + +\begin{itemize} + \item \textbf{v0.1.0}: Major overhaul + \begin{itemize} + \item Support for arbitrary arrow tips + \item Support for directly offsetting soft paths + \item New key \texttt{/tikz/scaling nfold} + \item The \texttt{decorations} library was dropped + \item Various performance improvements in \texttt{bezieroffset} (thanks to \hbox{Qrrbrbirlbel}) + \item Very short lines with large angles were fixed (e.g.\ in \texttt{tikzcd} with \texttt{squiggly}) + \item Numerous bugs fixed + \end{itemize} + \item \textbf{v0.0.1}: First public version +\end{itemize} + \appendix \newpage -\section{The offsetting algorithm} +\section{The Bézier offsetting algorithm} This algorithm is based on an algorithm by \href{https://github.com/Pomax/}{Pomax}. See \href{https://pomax.github.io/bezierinfo/#offsetting}{A Primer on Bézier curves}, the source code can be found \href{https://github.com/Pomax/bezierinfo/blob/bcfce2149fa5e5540a2a2605986adab3b2a9a3bf/js/graphics-element/lib/bezierjs/bezier.js}{here}. @@ -288,7 +370,7 @@ In order to offset an arbitrary Bézier curve we split it into fully simple segm \subsection{Subdivision} -It is well known that at every point $0 < t < 1$, a Bézier curve $A = (A_1, A_2, A_3, A_4)$ can be subdivided into two Bézier curves $B$ and $C$ using de Casteljau's algorithm (which naturally fulfil $A_1 = B_1$ and $A_4 = C_4$). A more or less heuristic fact is that $B$ and $C$ are ``more likely'' to be simple than $A$ (if you can prove any of the statements here, please contact me). Hence, if one wants to offset a non-simple curve $A$, one could try to subdivide $A$ until all of its segments are simple, then offset each segment. +It is well known that at every point $0 < t < 1$, de Casteljau's algorithm can subdivide a Bézier curve $A = (A_1, A_2, A_3, A_4)$ into two Bézier curves $B$ and $C$ (which naturally fulfil $A_1 = B_1$ and $A_4 = C_4$). A more or less heuristic fact is that $B$ and $C$ are ``more likely'' to be simple than $A$ (if you can prove any of the statements here, please contact me). Hence, if one wants to offset a non-simple curve $A$, one could try to subdivide $A$ until all of its segments are simple, then offset each segment. \subsection{Pomax' approach} The original approach by Pomax consists of two passes. The first pass subdivides $A$ on all extrema in $x$ or $y$. In a second pass, each segment $A^{(i)}$ is made simple in steps of $t \mapsto t + 0.01$, roughly using the following pseudocode: @@ -298,20 +380,20 @@ The original approach by Pomax consists of two passes. The first pass subdivides while t_2 < 1.0: S = segment(A from t_1 to t_2+0.01) if not isSimple(S): - segments += [S] + segments += [segment(A from t_1 to t_2)] t_1 = t_2 t_2 += 0.01 \end{verbatim} \end{samepage} -Essentially, this verifies with great certainty that the segment is fully simple in the sense of +Essentially, this ensures with great certainty that the segment is fully simple in the sense of \cref{def:fullySimpleCurve}. -The main reason this approach is not used in this library is performance, as the library is slow enough already. Other minor reasons include that the original approach is not invariant under reversals or rotations: Reversing and/or rotating a curve yields a different subdivision and hence potentially a slightly different-looking curve. +The main reason this approach is not used in this library is performance, since the loop is quite expensive computationally. Other minor reasons include that the original approach is not invariant under reversals or rotations: Reversing and/or rotating a curve yields a different subdivision and hence potentially a slightly different-looking curve. \subsection{The approach used here} \label{subsec:subdivisionUsed} -In this library, we instead take a recursive approach: +In this library we instead take a recursive approach: \begin{verbatim} - def split(A, level): + def makeSimple(A, level): if isSimple(A): segments += [A] else: @@ -320,8 +402,8 @@ In this library, we instead take a recursive approach: segments += [A] else: first, second = split(A, t=0.5) - split(first, level-1) - split(second, level-1) + makeSimple(first, level-1) + makeSimple(second, level-1) \end{verbatim} The default maximum depth is 5, so the curve is split into at most $2^5 = 32$ segments. This has the downside that some simple but not fully simple curves may remain undetected and be offset slightly incorrectly. If you encounter examples of such curves with bad outputs or if you have any ideas for additional constraints to add to \cref{def:simpleCurve} that can be checked with reasonable computational effort, please be in touch. @@ -330,7 +412,7 @@ The default maximum depth is 5, so the curve is split into at most $2^5 = 32$ se Disregarding edge cases (which will be discussed later), offsetting the curve works as follows: \begin{enumerate} - \item Construct lines orthogonal to the tangent in $A_1$ and $A_4$ and find their intersection. This point is called the \emph{origin} of the curve. + \item Construct lines orthogonal to the tangent in $A_1$ and $A_4$ and find their intersection. This point is called the \emph{origin} of the curve, denoted by $O$. \item The new control points $A'_1$ and $A'_4$ are given by $A_1$ and $A_4$ offset orthogonally to the tangent. \item Construct a ray from $A'_1$ parallel to the tangent in $A_1$, and construct another ray from the origin through $A_2$. Now $A'_2$ is given by the intersection of those rays. \item $A'_3$ can be constructed similarly. @@ -401,7 +483,7 @@ which is regular as $O$ approaches infinity. Now we to determine the inverse of \begin{equation} \frac{\sin(\tfrac{\pi}{2} - \alpha)}{\overline{OA_4}} = \frac{\sin(\alpha + \beta)}{\overline{A_1A_4}} = \frac{\sin(\tfrac{\pi}{2} - \beta)}{\overline{OA_1}} \implies \frac{1}{\overline{OA_1}} = \frac{1}{\overline{A_1A_4}}\cdot\frac{\sin(\alpha + \beta)}{\cos(\beta)} \ . \end{equation} -Note that $-\tfrac{\pi}{2} < \beta < \tfrac{\pi}{2}$ (and hence $\cos(\beta) > 0$) is guaranteed if the curve is simple --- in fact, simplicity guarantees $\alpha \cdot \beta > 0$ and $|\alpha| + |\beta| \leq \tfrac{\pi}{3}$. Using the sine addition theorem we can further rewrite the fraction to +Note that $-\tfrac{\pi}{3} \leq \beta \leq \tfrac{\pi}{3}$ (and hence $\cos(\beta) \geq \tfrac{1}{2}$) is guaranteed if the curve is simple --- in fact, simplicity guarantees $\alpha \cdot \beta > 0$ and $|\alpha| + |\beta| \leq \tfrac{\pi}{3}$. Using the sine addition theorem we can further rewrite the fraction to \begin{equation} \frac{\sin(\alpha + \beta)}{\cos(\beta)} = \sin(\alpha) + \cos(\alpha) \frac{\sin(\beta)}{\cos(\beta)} \ , \end{equation} @@ -423,7 +505,7 @@ Let furthermore $\uv_{ij} := \vv_{ij}/|\vv_{ij}|$ be the normalised vectors. The \subsection{Edge cases} -\subsubsection{Overlaps: $A_i = A_{i+1}$} +\subsubsection{Overlaps: \texorpdfstring{$A_i = A_{i+1}$}{A\_i = A\_(i+1)}} If there is one overlap $A_1=A_2$, $A_2=A_3$ or $A_3=A_4$, the cubic Bézier curve reduces to a quadratic one. For two overlaps, we get a linear Bézier curve (i.e.\ a straight line), and for three overlaps we get a point. The main problem to watch out for is that the tangents $\tv_0$ and $\tv_1$ need to be computed differently: \begin{itemize} @@ -434,7 +516,7 @@ If there is one overlap $A_1=A_2$, $A_2=A_3$ or $A_3=A_4$, the cubic Bézier cur \end{itemize} The analogous statement hold for $\tv_1$. In practice we test for approximate, not exact equality. -\subsubsection{Overlaps $A_1 = A_4$} +\subsubsection{Overlaps \texorpdfstring{$A_1 = A_4$}{A\_1 = A\_4}} \Cref{eq:finalPrimeDistance} has one remaining singularity, namely for $A_1 \approx A_4$. This singularity is fundamental and not an artefact: As $A_1$ approaches $A_4$ while $\overline{A_1A_2}$ and $\overline{A_3A_4}$ stay constant, $O$ also approaches $A_4$, hence the angle between $\overrightarrow{A_1A_2}$ and $\overrightarrow{OA_2}$ approaches zero, sending the intersection point $A'_2$ to infinity. diff --git a/graphics/pgf/contrib/tikz-nfold/tikzlibrarynfold.code.tex b/graphics/pgf/contrib/tikz-nfold/tikzlibrarynfold.code.tex index 8013d423f6..75c3cdc0d3 100644 --- a/graphics/pgf/contrib/tikz-nfold/tikzlibrarynfold.code.tex +++ b/graphics/pgf/contrib/tikz-nfold/tikzlibrarynfold.code.tex @@ -16,12 +16,10 @@ % This work consists of the files pgflibrarybezieroffset.code.tex, % tikzlibrarynfold.code.tex, tikz-nfold-doc.tex, and tikz-nfold-doc.pdf. -\usetikzlibrary{decorations} \usetikzlibrary{arrows.meta} -%\usepgflibrary{bezieroffset} \usepgflibrary{bezieroffset} -% + % % Intercepting join settings % -------------------------- @@ -63,63 +61,66 @@ % Various helper commands % ----------------------- -% check if a decoration segment is visible, i.e. not moveto or last +% check if a segment is visible, i.e. not moveto or last \newif\ifpgf@nfold@segm@visible -\def\ifpgfsegmentvisible#1{% +\def\checkpgfsegmentvisible#1{% \pgf@nfold@segm@visiblefalse% - \ifx#1\pgfdecorationinputsegmentlineto% - \pgf@nfold@segm@visibletrue\fi% - \ifx#1\pgfdecorationinputsegmentcurveto% - \pgf@nfold@segm@visibletrue\fi% - \ifx#1\pgfdecorationinputsegmentclosepath% - \pgf@nfold@segm@visibletrue\fi% - \ifpgf@nfold@segm@visible% + \ifx#1\pgf@nfold@inputsegmentlineto% + \pgf@nfold@segm@visibletrue% + \else\ifx#1\pgf@nfold@inputsegmentcurveto% + \pgf@nfold@segm@visibletrue + \else\ifx#1\pgf@nfold@inputsegmentclosepath% + \pgf@nfold@segm@visibletrue + \fi\fi\fi } +% % Joining offset lines % -------------------- % % One of the more difficult aspects is joining the segments of an offset path. Without this step, % the path would be interrupted or self-intersect whenever there is a non-zero angle between -% two segments. This code reproduces the existing line joins "bevel", "miter" and "round" as well -% as possible. +% two segments. This code reproduces the existing line joins "bevel", "miter" and "round". % \def\pgf@nfold@miterjoin{ - % - Although it isn't obvious, this code is invariant under - % \pgf@nfold@deltaphi -> \pgf@nfold@deltaphi + 2pi - % - The tip of the miter join is computed starting from the unshifted point and then moving + % The tip of the miter join is computed starting from the original (unshifted) centre of the join; + % we then move orthorgonal to the average of the old and new angle \pgfpointadd% - {\pgf@decorate@inputsegment@first}% + {\pgf@nfold@inputsegment@first}% {\pgfpointpolar% - {\pgf@nfold@cached@endangle+.5*\pgf@nfold@deltaphi+90}% - {\pgf@shiftdec@amount/cos(.5*\pgf@nfold@deltaphi)}% + {\pgf@nfold@previous@endangle+.5*\pgf@nfold@deltaphi@start+90}% do not change + {\pgf@nfold@shiftamount/cos(.5*\pgf@nfold@deltaphi@start)}% }% \pgfpathlineto{}% } \def\pgf@nfold@beveljoin{ - % The subsequent formula needs deltaphi to be between -180 and 180 - \pgfmathsetmacro{\pgf@nfold@deltaphi}{Mod(\pgf@nfold@deltaphi + 180, 360) - 180} - % For a good-looking result, inside lines should have a constant distance from each other all the way - % through the bevel join, which is a non-trivial condition. To achieve this result, the outer lines get - % a bevel-like join and the inner lines get a miter-like join; the threshold depends on the angle. + % The bevel join for one component line consists of three parts: + % 1) an extension of the ingoing line, + % 2) a middle line, angled at the average of the incoming and outgoing line, + % 3) an extension of the outgoing line. + % Different components of the ingoing and outgoing lines have a constant distance from each other. + % For a good-looking output, the mittle parts of the component lines thus should also have a constant distance, + % which is a non-trivial condition. To generate such an output the outer lines get a bevel-like join + % and the inner lines get a miter-like join; the threshold depends on deltaphi. % We first compute by how much the outermost line must be continued from the beginning of the join. % The protrusion amount must be lowered by a little bit for a rather complicated reason: The offset would be % dead on if the outermost offset line were centered on the _edge_ of the wide line, but we want to draw % the outside line _fully inside_ the wide line. The factor of tan(deltaphi/4) can be derived, but is not obvious. - \pgfmathsetmacro{\bevelouterprotrusion}{\pgf@nfold@shortenforjoin - .5*\pgflinewidth*abs(tan(.25*\pgf@nfold@deltaphi))} + \pgfmathsetlengthmacro{\bevelouterprotrusion}% + {\pgf@nfold@shortenstartjoin - .5*\pgflinewidth*abs(tan(.25*\pgf@nfold@deltaphi@start))} % The following applies to middle lines only: We compute by how much they need to be shortened so the distance % between the lines in the join is correct. - \pgfmathsetmacro{\bevelshorten}{2*\insidepercentage*abs(tan(.25*\pgf@nfold@deltaphi))} - % This theshold decides if the inside line is a bevel or a miter - \pgfmathparse{\bevelshorten < abs(sin(.5*\pgf@nfold@deltaphi))} + \pgfmathsetmacro{\bevelshorten}{2*\insidepercentage*abs(tan(.25*\pgf@nfold@deltaphi@start))} + % This threshold decides if the inside line has a bevel or a miter join + \pgfmathparse{\bevelshorten < abs(sin(.5*\pgf@nfold@deltaphi@start))} \ifnum\pgfmathresult=1\relax - \pgfmathsetmacro{\bevelextension}{\bevelouterprotrusion-\bevelshorten*\pgf@decoration@nfold@hwidth} - \pgfpointadd{\pgf@nfold@offset@start}{\pgfpointpolar{\pgf@nfold@cached@endangle}{\bevelextension}} + \pgfmathsetlengthmacro{\bevelextension}{\bevelouterprotrusion-\bevelshorten*\pgf@nfold@hwidth} + \pgfpointadd{\pgf@nfold@join@start}{\pgfpointpolar{\pgf@nfold@previous@endangle}{\bevelextension}} \pgfpathlineto{} - \pgfpointadd{\pgf@nfold@offset@end}{\pgfpointpolar{\pgfdecoratedinputsegmentstartangle}{-\bevelextension}} + \pgfpointadd{\pgf@nfold@join@end}{\pgfpointpolar{\pgf@nfold@cur@startangle}{-\bevelextension}} \pgfpathlineto{} \else \pgf@nfold@miterjoin @@ -129,49 +130,63 @@ \def\pgf@nfold@roundjoin{ % The outer half of the lines get arcs, the others get miters \ifdim\insidepercentage pt<.5pt\relax - \pgfpointadd{\pgf@nfold@offset@start}{\pgfpointpolar{\pgf@nfold@cached@endangle}{\pgf@nfold@shortenforjoin}} + \pgfpointadd{\pgf@nfold@join@start}{\pgfpointpolar{\pgf@nfold@previous@endangle}{\pgf@nfold@shortenstartjoin}} \pgfpathlineto{} % Check if the angles are in the correct range; under some conditions we must add or subtract 360 - \pgfmathparse{\turnindicator*(\pgfdecoratedinputsegmentstartangle-\pgf@nfold@cached@endangle)} + \pgfmathparse{\turnindicator*(\pgf@nfold@cur@startangle-\pgf@nfold@previous@endangle)} \ifdim\pgfmathresult pt>0pt\relax - \pgfmathsetmacro{\targetang}{\pgfdecoratedinputsegmentstartangle-\turnindicator*360} + \pgfmathsetmacro{\targetang}{\pgf@nfold@cur@startangle-\turnindicator*360} \else - \let\targetang\pgfdecoratedinputsegmentstartangle + \let\targetang\pgf@nfold@cur@startangle \fi \pgfpatharc% - {\pgf@nfold@cached@endangle+90*\turnindicator}% + {\pgf@nfold@previous@endangle+90*\turnindicator}% {\targetang+90*\turnindicator}% - {abs(\pgf@shiftdec@fraction)*\pgf@decoration@nfold@hwidth}% + {abs(\pgf@nfold@shift@fraction)*\pgf@nfold@hwidth}% \else \pgf@nfold@miterjoin \fi } \def\pgf@nfold@make@join{ - % The code must be invariant under deltaphi -> deltaphi + 360, which can be verified experimentally - \pgfmathsetmacro{\pgf@nfold@deltaphi}{\pgfdecoratedinputsegmentstartangle-\pgf@nfold@cached@endangle} - % Offset the start and end of this segment - \pgfpointadd{\pgf@nfold@cached@endpoint}{\pgfpointpolar{\pgf@nfold@cached@endangle+90}{\pgf@shiftdec@amount}} - \pgfstorepoint\pgf@nfold@offset@start - \pgfpointadd{\pgf@nfold@segment@start}{\pgfpointpolar{\pgfdecoratedinputsegmentstartangle+90}{\pgf@shiftdec@amount}} - \pgfstorepoint\pgf@nfold@offset@end - \pgfpointdiff{\pgf@nfold@offset@start}{\pgf@nfold@offset@end} - % Check if the start of this segment is too close to the end of the previous segment. - % In that case we don't insert a join segment, as it would look rather glitchy. - % We use the Manhattan length for performance and to avoid 'dimension too large' errors. - \pgfmathparse{abs(\pgf@x) + abs(\pgf@y)} - \ifdim\pgfmathresult pt>0.1pt\relax - % First step: Check if left or right turn (-1 = left, 1=right) - \pgfmathsetmacro{\turnindicator}{sign(sin(\pgf@nfold@cached@endangle-\pgfdecoratedinputsegmentstartangle)} - % between 0 and 1; 0=no distance to cover in the join, 1=maximum distance to cover - \pgfmathsetmacro{\insidepercentage}{-.5*\turnindicator*\pgf@shiftdec@fraction+.5} - \if m\pgf@cached@linejoin % because both are only one character, \ifx is not needed + % Offset the start and end of this join + \pgfextract@process\pgf@nfold@join@start{% + \pgfpointadd{\pgf@nfold@previous@joinend} + {\pgfpointpolar{\pgf@nfold@previous@endangle+90}{\pgf@nfold@shiftamount}}}% + \pgfextract@process\pgf@nfold@join@end{% + \pgfpointadd{\pgf@nfold@segment@start} + {\pgfpointpolar{\pgf@nfold@cur@startangle+90}{\pgf@nfold@shiftamount}}}% + \pgf@process{\pgfpointdiff{\pgf@nfold@join@start}{\pgf@nfold@join@end}} + % Check if the start of this segment is very close to the end of the previous segment. + % In that case we don't need a join at all + \pgfpointtaxicabnorm\pgf@xa + \ifdim\pgf@xa>0.1pt\relax + % First step: Check if left or right turn (-1=left, 1=right) + \ifdim\pgf@nfold@deltaphi@start pt<0pt + \def\turnindicator{1} + \else + \def\turnindicator{-1} + \fi + % \insidepercentage: between 0.0 and 1.0; + % 0=no distance to cover in the join, 1=maximum distance to cover + \pgf@xa=\pgf@nfold@shift@fraction pt\relax + \pgf@xa=\turnindicator\pgf@xa + \advance\pgf@xa by-1pt\relax + \pgf@xa=-.5\pgf@xa + % \insidepercentage = .5 * (1 - \turnindicator*\pgf@nfold@shift@fraction) + \edef\insidepercentage{\pgf@sys@tonumber\pgf@xa} + \if m\pgf@cached@linejoin% \ifx is not needed because both are only one character % miter join % First we implement the miter limit: If the angle is too sharp, the miter join is replaced % by a bevel join. This is controlled by /tikz/miter limit=..., initially 10. - \pgfmathsetmacro{\mitercosine}{abs(cos(.5*\pgf@nfold@deltaphi))} - \pgfmathparse{\mitercosine*\pgf@nfold@cached@miterlimit} - \ifdim\pgfmathresult pt>1pt\relax + \pgf@xa=\pgf@nfold@deltaphi@start pt\relax + \pgf@xa=.5\pgf@xa + \pgfmathcos@{\pgf@sys@tonumber\pgf@xa} + \pgf@xa=\pgfmathresult pt\relax + \pgf@xa=\pgf@nfold@cached@miterlimit\pgf@xa + % Check if miterlimit*cos(.5*deltaphi) > 1; + % cos(.5*deltaphi) >= 0 because -180 <= deltaphi <= 180 + \ifdim\pgf@xa>1pt\relax \pgf@nfold@miterjoin \else \pgf@nfold@beveljoin @@ -186,600 +201,933 @@ \fi \fi \fi - % Common for all non-trivial joins: Connect to the starting point of the current segment. - % Note that this code is skipped by the global \ifdim if the start and end of the join coincide, - % so we never get a zero length segment here. - \pgfpathlineto{\pgf@nfold@offset@end} + % All non-trivial joins connect to the end of the join, which is the starting point of the current segment. + % Note that this entire macro is skipped by if the start and end of the join coincide, so we never create a zero length segment here. + % + % There is one edge case here: If two subsequent joins are so close that \pgf@nfold@segment@end + % and \pgf@nfold@segment@start exchange places *and* we are on the outside of the join + % (implying that we are on the inside of the next join), \pgf@nfold@join@end will be located behind the next join. + % In this case we instead connect to the offset of \pgf@nfold@segment@end (which comes *before* \pgf@nfold@segment@start). + \pgf@nfold@join@end% default point to connect to + \ifpgf@nfold@closejoinsedgecase% + \ifdim\insidepercentage pt<.5pt\relax% + \pgf@process{\pgfpointadd% + {\pgf@nfold@segment@end}% + {\pgfpointpolar{\pgf@nfold@cur@startangle+90}{\pgf@nfold@shiftamount}}} + \fi% + \fi + \pgfpathlineto{} \fi } % -% Internal offset decoration -% -------------------------- -% -% This internal tikz decoration offsets a given path (but doesn't draw it multiple times). +% Main rendering pipeline +% ----------------------- % -% pgfkeys interface, used in the decorations below -\newcount\pgf@nfold@dec@order -\pgf@nfold@dec@order=2 -\pgfkeys{ - % - use a counter so we get an error if something other than a number is provided - % - if needed, throw an error that the intersection library needs to be loaded - /pgf/decoration/nfold order/.code={ - \pgf@nfold@dec@order=#1 - \ifnum\pgf@nfold@dec@order>\tikz@arrow@intersec@numcached - \ifdefined\pgfintersectionofpaths\else - \pgferror{For 'nfold' larger than \tikz@arrow@intersec@numcached\space you need to say \string\usetikzlibrary{intersections}} - \fi - \fi - }, - /pgf/decoration/nfold width/.code={\pgfmathsetlengthmacro{\pgf@decoration@nfold@hwidth}{.5*#1}}, - % integer between 1 and \pgf@nfold@dec@order - /pgf/decoration/nfold index/.store in=\pgf@nfold@index -} - -% In here we cache the end angle of the previous decorated segment, which is otherwise inaccessible -\newdimen\pgf@nfold@cached@endangle % This stores whether the current segment should begin with a moveto to its offset \newif\ifpgf@nfold@continuesegment +% This stores whether we are in some edge case of very close joins, see below for details +\newif\ifpgf@nfold@closejoinsedgecase +% This stores whether we are in an error case where we need to avoid dividing by zero +\newif\ifpgf@nfold@angletoosharp +% This stores whether we need the intersections library for an arrow tip but it is not loaded +\newif\ifpgf@nfold@intersectionsnotloaded + -% some required computations for the current segment -\def\pgf@nfold@shift@prepare@segment{% - \pgftransformreset % important! otherwise we work in the local coordinate system - % compute the relative deviation from the original path (between -1.0 and 1.0) - \pgfmathsetmacro{\pgf@shiftdec@fraction}% - {-1+2*(\pgf@nfold@index-1)/(\pgf@nfold@dec@order-1)} - \pgfmathsetlengthmacro{\pgf@shiftdec@amount}{\pgf@decoration@nfold@hwidth*\pgf@shiftdec@fraction} +\def\pgf@nfold@handlesegment{% + %%% Step 0: Preparation % Set some default values; they might get overwritten later - \let\pgf@nfold@segment@start\pgf@decorate@inputsegment@first - \let\pgf@nfold@segment@end\pgf@decorate@inputsegment@last + \let\pgf@nfold@segment@start\pgf@nfold@inputsegment@first + \let\pgf@nfold@segment@end\pgf@nfold@inputsegment@last % Set a default value for \ifpgf@nfold@continuesegment. % It may be overwritten by \pgf@nfold@extendtotip - \ifx\pgfdecorationpreviousinputsegment\pgfdecorationinputsegmentmoveto + \ifx\pgf@nfold@previousinputsegment\pgf@nfold@inputsegmentmoveto \pgf@nfold@continuesegmentfalse \else \pgf@nfold@continuesegmenttrue \fi -} - -% draw the current segment including a join at the start (if present) -\def\pgf@nfold@shift@handle@segment{% + %%% Step 1: Make space for joins if necessary % In order to make space for the join, it may be necessary to shorten the current segment % at the start and/or the end. In here we store by how much the segment needs to be shortened. - \def\pgf@nfold@shortenforjoin{0pt} + \def\pgf@nfold@shortenstartjoin{0pt} + \def\pgf@nfold@shortenendjoin{0pt} + \pgf@nfold@closejoinsedgecasefalse + \pgf@nfold@angletoosharpfalse % Make a join only if two adjacent segments are both visible - \ifpgfsegmentvisible\pgfdecorationcurrentinputsegment - % Step 1: Make space for the join at the start if needed - \ifx\pgfdecorationpreviousinputsegment\pgfdecorationinputsegmentmoveto\else - \ifdefined\pgfdecorationpreviousinputsegment - % As far as I am aware, the previous input segment is either moveto or undefined - \pgfwarning{'nfold': Unexpected previous input segment in decoration 'nfold' (\meaning\pgfdecorationpreviousinputsegment)} + \checkpgfsegmentvisible\pgf@nfold@currentinputsegment + \ifpgf@nfold@segm@visible% + % Step 1.1: Make space for the join at the start if needed + \ifx\pgf@nfold@previousinputsegment\pgf@nfold@inputsegmentmoveto\else + \pgf@xb=\pgf@nfold@deltaphi@start pt\relax + \ifdim\pgf@xb<0pt\relax + \pgf@xb=-\pgf@xb \fi - \pgf@xa=\pgfdecoratedinputsegmentstartangle pt\relax - \advance\pgf@xa by-\pgf@nfold@cached@endangle\relax - \pgfmathparse{abs(cos(0.5*\pgf@xa))} - \ifdim\pgfmathresult pt<0.02pt\relax - % we go full backwards, don't relocate the start - \pgfwarning{Angle too sharp in decoration 'nfold', expect visual errors} + \ifdim\pgf@xb>178pt\relax + % we go full backwards, don't relocate the start and disable the join to avoid division by zero + \pgfutil@packagewarning{tikz-nfold}{Angle too sharp, expect visual errors} + \pgf@nfold@angletoosharptrue \else - \pgfmathsetlengthmacro{\pgf@nfold@shortenforjoin}{\pgf@decoration@nfold@hwidth*abs(tan(0.5*\pgf@xa))} - \pgfpointadd{\pgf@decorate@inputsegment@first}{\pgfpointpolar{\pgfdecoratedinputsegmentstartangle}{\pgf@nfold@shortenforjoin}} - \pgfstorepoint\pgf@nfold@segment@start + \ifdim\pgf@xb>0.5pt\relax + % make space for the start join if the angle is nonzero + \pgf@yb=.5\pgf@xb + \pgfmathtan@{\pgf@sys@tonumber\pgf@yb} + \pgf@yb=\pgf@nfold@hwidth\relax + \pgf@yb=\pgfmathresult\pgf@yb + % shortenstartjoin = hwidth*tan(.5*abs(deltaphi@start)) + \edef\pgf@nfold@shortenstartjoin{\the\pgf@yb} + \pgfextract@process\pgf@nfold@segment@start{% + \pgfpointadd{\pgf@nfold@inputsegment@first}% + {\pgfqpointpolar{\pgf@nfold@cur@startangle}{\pgf@yb}}}% + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentcurveto + % For a curve we need to relocate the @supporta point as well, as otherwise the @first point + % might overtake it + \pgf@process{\pgfpointdiff{\pgf@nfold@inputsegment@first}{\pgf@nfold@inputsegment@supporta}} + \pgfmathveclen@{\pgf@sys@tonumber\pgf@x}{\pgf@sys@tonumber\pgf@y} + \ifdim\pgfmathresult pt>0.1pt\relax + % regular curves (supporta != first): + % Increase dist(first, supporta) to sqrt(a^2 + b^2) where a=dist(first, supporta) and b=shortenstart. This way, the order of first and supporta is guaranteed to be preserved, and the change to supporta is as small as reasonably possible. + \pgfmathveclen@{\pgfmathresult}{\pgf@sys@tonumber\pgf@yb} + \pgfextract@process\pgf@nfold@inputsegment@supporta{\pgfpointadd% + {\pgf@nfold@inputsegment@first}% + {\pgfqpointpolar{\pgf@nfold@cur@startangle}{\pgfmathresult pt}}}% + \else + % special treatment for singular curves (supporta = first) to avoid rounding error glitches. + % In this special case, a slight corner at the end of the join is unavoidable + \let\pgf@nfold@inputsegment@supporta\pgf@nfold@segment@start + \fi + \fi + \fi \fi \fi - % Step 2: Make space for the join at the end if needed - \ifpgfsegmentvisible\pgfdecorationnextinputsegmentobject - % for reasons unknown to me, - % \pgfdecoratedangletonextinputsegment has a "pt" but the others do not - \pgf@xa=\pgfdecoratedangle pt\relax - \advance\pgf@xa by \pgfdecoratedangletonextinputsegment\relax - \advance\pgf@xa by -\pgfdecoratedinputsegmentendangle pt\relax - \pgfmathparse{abs(cos(0.5*\pgf@xa))} - \ifdim\pgfmathresult pt<0.02pt\relax - \pgfwarning{Angle too sharp in decoration 'nfold', expect visual errors} + % Step 1.2: Make space for the join at the end if needed + \checkpgfsegmentvisible\pgf@nfold@next@segmenttype + \ifpgf@nfold@segm@visible% + \pgf@xb=\pgf@nfold@deltaphi@end pt\relax + \ifdim\pgf@xb<0pt\relax + \pgf@xb=-\pgf@xb + \fi + \ifdim\pgf@xb>178pt\relax + \pgfutil@packagewarning{tikz-nfold}{Angle too sharp, expect visual errors} + \pgf@nfold@angletoosharptrue \else - \pgfmathparse{-\pgf@decoration@nfold@hwidth*abs(tan(0.5*\pgf@xa))} - \pgfpointadd{\pgf@decorate@inputsegment@last}{\pgfpointpolar{\pgfdecoratedinputsegmentendangle}{\pgfmathresult pt}} - \pgfstorepoint\pgf@nfold@segment@end + \ifdim\pgf@xb>0.5pt\relax + % make space for the start join if the angle is nonzero + \pgf@yb=.5\pgf@xb + \pgfmathtan@{\pgf@sys@tonumber\pgf@yb} + \pgf@yb=\pgf@nfold@hwidth\relax + \pgf@yb=\pgfmathresult\pgf@yb + % shortenendjoin = hwidth*tan(.5*abs(deltaphi@end)) + \edef\pgf@nfold@shortenendjoin{\the\pgf@yb} + \pgf@yb=-\pgf@yb + \pgfextract@process\pgf@nfold@segment@end{% + \pgfpointadd{\pgf@nfold@inputsegment@last}% + {\pgfqpointpolar{\pgf@nfold@cur@endangle}{\pgf@yb}}}% + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentcurveto + % Same procedure as above: relocate supportb if we have a curve + \pgf@process{\pgfpointdiff{\pgf@nfold@inputsegment@supportb}{\pgf@nfold@inputsegment@last}} + \pgfmathveclen@{\pgf@sys@tonumber\pgf@x}{\pgf@sys@tonumber\pgf@y} + \ifdim\pgfmathresult pt>0.1pt\relax + \pgfmathveclen@{\pgfmathresult}{\pgf@sys@tonumber\pgf@yb} + % can use qpoint and minus because \pgfmathresult is guaranteed to be positive + \pgfextract@process\pgf@nfold@inputsegment@supportb{\pgfpointadd% + {\pgf@nfold@inputsegment@last}% + {\pgfqpointpolar{\pgf@nfold@cur@endangle}{-\pgfmathresult pt}}}% + \else + \let\pgf@nfold@inputsegment@supportb\pgf@nfold@segment@end + \fi + \fi + \fi \fi - \fi + % Step 1.3: Detect an edge case + % This edge case appears whenever the current segment is such a short line that we would + % have to reduce its length to less than zero to make space for the joins. In such cases, + % the line is not drawn at all, and slight modifications must be made to the joins to ensure + % a correct output (i.e. one join is immediately followed by the next without a segment in between). + % + % This edge case can appear for curves as well, but they are much harder to deal with. + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentlineto + % No need to check for \pgf@nfold@inputsegmentclosepath as it should not be followed by any further segments + % Now: Check if the total amount of shortening is larger than the length of the segment + \pgfpointdiff{\pgf@nfold@inputsegment@first}{\pgf@nfold@inputsegment@last} + \pgfmathveclen@{\pgf@sys@tonumber\pgf@x}{\pgf@sys@tonumber\pgf@y} + \pgf@xa=\pgf@nfold@shortenstartjoin\relax + \advance\pgf@xa by\pgf@nfold@shortenendjoin\relax + \ifdim\pgf@xa>\pgfmathresult pt\relax + \pgf@nfold@closejoinsedgecasetrue + \fi + \fi + \fi% end if next segment visible % - % Step 3: Draw the join at the start if applicable + % Step 2.1: Draw the join at the start if applicable % - \ifx\pgfdecorationpreviousinputsegment\pgfdecorationinputsegmentmoveto\else - \pgf@nfold@make@join + \ifx\pgf@nfold@previousinputsegment\pgf@nfold@inputsegmentmoveto + \ifpgf@nfold@closejoinsedgecase + % If the previous segment is a moveto and the current segment is a "close joins" edge case, + % nothing needs to be drawn here (the relevant draw call will be made at the join of the subsequent + % segment). We must therefore make sure that we move to the correct end point of this segment. + % Counterintuitively, this is given by the offset of \pgf@nfold@segment@start since the start and end + % are reversed in the edge case. + \pgfpointadd% + {\pgf@nfold@segment@start}% + {\pgfpointpolar{\pgf@nfold@cur@startangle+90}{\pgf@nfold@shiftamount}} + \pgfpathmoveto{} + \fi + \else + % If we draw the join when the start angle is close to 180 degrees, we get a division by zero + \ifpgf@nfold@angletoosharp\else + \pgf@nfold@make@join + \fi \fi - \fi + \fi% end if current segment visible + % Step 2.2: Store where the current (non-offset) end point was relocated + % in order to make space for the end join. This may be used if the next + % segment begins with a join + \let\pgf@nfold@previous@joinend\pgf@nfold@segment@end % - % Step 4: Draw the new segment. + % Step 3: Draw the new segment. % % The value of \ifpgf@nfold@continuesegment decides whether we start with a moveto. - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentlineto - \ifpgf@nfold@continuesegment - \pgfoffsetlinenomove{\pgf@nfold@segment@start}{\pgf@nfold@segment@end}{\pgf@shiftdec@amount} - \else - \pgfoffsetline{\pgf@nfold@segment@start}{\pgf@nfold@segment@end}{\pgf@shiftdec@amount} + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentlineto + \ifpgf@nfold@closejoinsedgecase\else + \ifpgf@nfold@continuesegment + \pgfoffsetlinenomove{\pgf@nfold@segment@start}{\pgf@nfold@segment@end}{\pgf@nfold@shiftamount} + \else + \pgfoffsetline{\pgf@nfold@segment@start}{\pgf@nfold@segment@end}{\pgf@nfold@shiftamount} + \fi \fi \fi - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentclosepath - \pgfwarning{'nfold': The option 'cycle' is not yet properly supported.} - \ifpgf@nfold@continuesegment - \pgfoffsetlinenomove{\pgf@nfold@segment@start}{\pgf@nfold@segment@end}{\pgf@shiftdec@amount} - \else - \pgfoffsetline{\pgf@nfold@segment@start}{\pgf@nfold@segment@end}{\pgf@shiftdec@amount} - \fi + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentclosepath + % Incomplete: proper join is missing + % Idea: when parsing the path, add a detection if there is a closepath at the current segment, + % then cache the data of the last segment. + % We could then insert a "fake previous segment" into the path at the right place + \pgfutil@packagewarning{tikz-nfold}{The option `cycle' is not yet properly supported} + \pgfpathclose \fi - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentcurveto + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentcurveto \ifpgf@nfold@continuesegment - \pgfoffsetcurvenomove{\pgf@nfold@segment@start}{\pgf@decorate@inputsegment@supporta}{\pgf@decorate@inputsegment@supportb}{\pgf@nfold@segment@end}{\pgf@shiftdec@amount} + \pgfoffsetcurvenomove{\pgf@nfold@segment@start}{\pgf@nfold@inputsegment@supporta}{\pgf@nfold@inputsegment@supportb}{\pgf@nfold@segment@end}{\pgf@nfold@shiftamount} \else - \pgfoffsetcurve{\pgf@nfold@segment@start}{\pgf@decorate@inputsegment@supporta}{\pgf@decorate@inputsegment@supportb}{\pgf@nfold@segment@end}{\pgf@shiftdec@amount} + \pgfoffsetcurve{\pgf@nfold@segment@start}{\pgf@nfold@inputsegment@supporta}{\pgf@nfold@inputsegment@supportb}{\pgf@nfold@segment@end}{\pgf@nfold@shiftamount} \fi \fi - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentlast - % Seems like this never happens - \pgfwarning{'nfold': Unexpected state (last part in segment state).} - \fi - % cache the end angle and end point because we need it on the next pass - \global\pgf@nfold@cached@endangle=\pgfdecoratedinputsegmentendangle pt\relax - \ifx\pgfdecorationnextinputsegmentobject\pgfdecorationinputsegmentmoveto\else - \global\let\pgf@nfold@cached@endpoint\pgf@nfold@segment@end + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentmoveto + % Every moveto is executed in the offsetting of the subsequent draw command, + % therefore we do not need a \pgfpathmoveto call here + \ifx\pgf@nfold@previousinputsegment\pgf@nfold@inputsegmentfirst + % Draw the tip extension at the start (if present) + \ifnum\pgf@nfold@start@arrowcode=1 + \pgf@nfold@extendtotip{s}% + \fi% + \fi \fi - % Step 5: Extend into the arrow tip at the end (if present) - \ifx\pgfdecorationnextinputsegmentobject\pgfdecorationinputsegmentlast% - \pgf@nfold@check@tip@end% - \ifpgf@nfold@tip@implies% + % Step 4: Extend into the arrow tip at the end (if present) + \ifx\pgf@nfold@next@segmenttype\pgf@nfold@inputsegmentlast% + \ifnum\pgf@nfold@end@arrowcode=1 \pgf@nfold@extendtotip{e}% \fi% \fi% } -\pgfdeclaredecoration{pgf@nfold@shift}{start}{% - \state{start}[width=\pgfdecoratedinputsegmentremainingdistance, next state=segment]{% - \egroup\begingroup - \pgf@nfold@shift@prepare@segment - \pgf@nfold@check@tip@start% - \ifpgf@nfold@tip@implies% - \pgf@nfold@extendtotip{s}% - \fi - \pgf@nfold@shift@handle@segment - \endgroup\bgroup% - } - \state{segment}[width=\pgfdecoratedinputsegmentremainingdistance, next state=segment]{% - \egroup\begingroup - \pgf@nfold@shift@prepare@segment - \pgf@nfold@shift@handle@segment - \endgroup\bgroup% - }% -}% - +% Rendering arrow tips +% -------------------- -% Internal pre-pass decoration -% ---------------------------- -% -% This internal decoration is run as the first step in the decoration 'nfold'. -% It caches some data that won't be accessible later, and it also makes space for the arrow tips -% if needed. +% Precomputed intersections % +% For arrows of order n > 2 with an Implies tip, the constituent parts of the n-fold arrow +% end somewhere in the middle of the tip. The exact end point must be computed using +% the intersections library. To speed up compilation times, the intersection points are precomputed +% up to n = 5. If your document contains arrows of order 6 or larger, consider adding those +% as well; the values are output in the log file. +\expandafter\def\csname pgf@nfold@intersec@cache@2@3\endcsname{\pgfqpoint{2pt}{0pt}} +\expandafter\def\csname pgf@nfold@intersec@cache@2@4\endcsname{\pgfqpoint{0.94063pt}{-0.33333pt}} +\expandafter\def\csname pgf@nfold@intersec@cache@3@4\endcsname{\pgfqpoint{0.94063pt}{0.33333pt}} +\expandafter\def\csname pgf@nfold@intersec@cache@2@5\endcsname{\pgfqpoint{0.64167pt}{-0.5pt}} +\expandafter\def\csname pgf@nfold@intersec@cache@3@5\endcsname{\pgfqpoint{2pt}{0pt}} +\expandafter\def\csname pgf@nfold@intersec@cache@4@5\endcsname{\pgfqpoint{0.64167pt}{0.5pt}} +% intersections are precomputed up to this order +\def\pgf@nfold@intersec@numcached{5} -\def\pgf@nfold@storedata@handle@segment{% - \let\pgf@nfold@segment@end\pgf@decorate@inputsegment@last% - \ifx\pgfdecorationnextinputsegmentobject\pgfdecorationinputsegmentlast% - % Implementing shorten > and making space for the arrow tip (if present) - \pgf@nfold@check@tip@end% - \ifpgf@nfold@tip@implies% - \pgfmathparse{-\pgf@shorten@end@additional-2.06*\pgf@decoration@nfold@hwidth-0.5*\pgflinewidth} - \else - \pgfmathparse{-\pgf@shorten@end@additional} - \fi - % This is the point where the arrow body ends - \pgfpointadd% - {\pgf@decorate@inputsegment@last}% - {\pgfpointpolar{\pgfdecoratedinputsegmentendangle}{\pgfmathresult pt}}% - \pgfstorepoint\pgf@nfold@segment@end% - % This is the tip of the arrow, required for drawing the arrow head - \pgfpointadd% - {\pgf@decorate@inputsegment@last}% - {\pgfpointpolar% - {\pgfdecoratedinputsegmentendangle}% - {-\pgf@shorten@end@additional}}% - \pgfglobalstorepoint\pgf@nfold@original@last% - \fi% - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentmoveto% - \pgfpathmoveto{\pgf@nfold@segment@end}% - \fi% - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentlineto% - \pgfpathlineto{\pgf@nfold@segment@end}% - \fi% - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentclosepath% - \pgfpathlineto{\pgf@nfold@segment@end}% - \fi% - \ifx\pgfdecorationcurrentinputsegment\pgfdecorationinputsegmentcurveto% - \pgfpathcurveto% - {\pgf@decorate@inputsegment@supporta}% - {\pgf@decorate@inputsegment@supportb}% - {\pgf@nfold@segment@end}% - \fi% -} - -\pgfdeclaredecoration{pgf@nfold@storedata}{start}{% - \state{start}[width=\pgfdecoratedinputsegmentremainingdistance, next state=segment]{% - \egroup\begingroup - \pgftransformreset - \iftikz@mode@double\else - \pgferror{Must enable /tikz/double to use 'nfold' (e.g. using [double distance=5pt]).} - \fi - % Cache arrow settings - \pgf@nfold@storearrows - % Cache inner and outer line width from the '/tikz/double' settings. These are stored in - % the macro \tikz@double@setup. Internally, the value of \pgfinnerlinewidth determines whether - % double stroke is enabled or not. Calling \tikz@double@setup changes line widths globally, so - % encapsulating the calls in a group would still have side effects. What we do instead is call - % \tikz@double@setup and then restore \pgflinewidth. - \tikz@double@setup - \pgfmathsetlengthmacro{\pgf@nfold@temp@fullwidth}{.25*(\pgflinewidth+\pgfinnerlinewidth)} - \global\edef\pgf@decoration@nfold@hwidth{\pgf@nfold@temp@fullwidth} - % We can either store \pgflinewidth before calling \tikz@double@setup, or we can reconstruct - % its old value from the new values. We do the latter here. - \pgfmathparse{.5*(\pgflinewidth-\pgfinnerlinewidth)} - \pgfsetlinewidth{\pgfmathresult pt} - % shorten < and shorten > do not work well with this decoration for various reasons. - % We "bake" them into the path in the pre-pass and then disable them for the rendering passes. - \pgfpointadd% - {\pgf@decorate@inputsegment@first}% - {\pgfpointpolar{\pgfdecoratedinputsegmentstartangle}{\pgf@shorten@start@additional}} - % store the tip of the arrow - \pgfglobalstorepoint\pgf@nfold@original@first% - % make space if applicable - \pgf@nfold@check@tip@start% - \ifpgf@nfold@tip@implies% - \pgfmathparse{\pgf@shorten@start@additional+2.06*\pgf@decoration@nfold@hwidth+0.5*\pgflinewidth} +% This macro extends the arrow body to the tips +% parameter: s=start, e=end +\def\pgf@nfold@extendtotip#1{ + \ifpgf@nfold@intersectionsnotloaded + \pgfutil@packageerror{tikz-nfold}{% + If `nfold' is larger than \pgf@nfold@intersec@numcached\space and you use + an `Implies' arrow tip you need to say \string\usetikzlibrary{intersections}}{} + \else + % Do not extend the arrow for index=1 and index=order, it already ends in the right place + \ifnum\pgf@nfold@index>1\relax\ifnum\pgf@nfold@index<\pgf@nfold@order\relax% + % Step 1: Find the intersection of the arrow's path with the head + \ifcsname pgf@nfold@intersec@cache@\the\pgf@nfold@index @\the\pgf@nfold@order\endcsname + \pgfextract@process\pgf@nfold@arrowintersect + {\csname pgf@nfold@intersec@cache@\the\pgf@nfold@index @\the\pgf@nfold@order\endcsname}% + \else + % the intersection has not been precomputed, thus compute on the fly here + \pgfintersectionofpaths{ + % specify the tip + \pgfpathmoveto{\pgfqpoint{-1.4pt}{2.65pt}} + \pgfpathcurveto{\pgfqpoint{-0.75pt}{1.25pt}}{\pgfqpoint{1pt}{0.05pt}}{\pgfqpoint{2pt}{0pt}} + \pgfpathcurveto{\pgfqpoint{1pt}{-0.05pt}}{\pgfqpoint{-0.75pt}{-1.25pt}}{\pgfqpoint{-1.4pt}{-2.65pt}} + }{ + % extend the body to intersect the tip + \pgfpathmoveto{\pgfqpoint{-3pt}{\pgf@nfold@shift@fraction pt}} + \pgfpathlineto{\pgfqpoint{3pt}{\pgf@nfold@shift@fraction pt}} + } + \ifnum\pgfintersectionsolutions>0 + \pgfextract@process\pgf@nfold@arrowintersect{\pgfpointintersectionsolution{1}}% + \immediate\write17{tikz-nfold: computed intersection cache@\the\pgf@nfold@index @\the\pgf@nfold@order: \string\pgfqpoint{\the\pgf@x}{\the\pgf@y}^^J} + % add the new intersection to the cache + \expandafter\xdef\csname pgf@nfold@intersec@cache@\the\pgf@nfold@index @\the\pgf@nfold@order\endcsname{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}} + \else + % this is a failsafe and should never be reached + \pgfutil@packagewarning{tikz-nfold}{did not find intersection} + \pgfextract@process\pgf@nfold@arrowintersect{\pgfqpoint{0pt}{\pgf@nfold@shift@fraction pt}}% + \fi + \fi% if precomputed + % Step 2: Extend the arrow body to the intersection point. + % If the tip is at the beginning of the path, we have to move to the intersection + % and then draw a line to the "regular" starting point. The subsequent segment then + % should omit its moveto. + % If the tip is at the end, we are already in the right position and only need to extend + % the current path to the intersection point. + \begingroup + \pgftransformreset + \if#1s + \pgftransformshift{\pgf@nfold@segment@start} + \pgftransformrotate{\pgf@nfold@next@startangle} + \pgftransformxscale{-1} + \else + \pgftransformshift{\pgf@nfold@segment@end} + \pgftransformrotate{\pgf@nfold@cur@endangle} + \fi + % we don't want to undo the shift by .42\pgflinewidth after the scaling + \pgfutil@tempdima=\pgf@nfold@hwidth + \pgfutil@tempdima=\pgf@nfold@shift@fraction\pgfutil@tempdima + \pgfextract@process\pgf@nfold@startofextension + {\pgfpointtransformed{\pgfqpoint{0pt}{\pgfutil@tempdima}}} + % 0.5 - 0.06 = 0.42 + \pgftransformshift{\pgfqpoint{.42\pgflinewidth}{0pt}} + \pgftransformscale{\pgf@nfold@hwidth} + \pgfextract@process\pgf@nfold@arrowintersect{\pgfpointtransformed{\pgf@nfold@arrowintersect}} + \global\let\pgf@nfold@startofextension\pgf@nfold@startofextension + \global\let\pgf@nfold@arrowintersect\pgf@nfold@arrowintersect + \endgroup + \if#1s + \pgfpathmoveto{\pgf@nfold@arrowintersect} + % This is precisely the start of the body, shifted vertically + \pgfpathlineto{\pgf@nfold@startofextension} + % hack: We make the next segment believe that this segment was a lineto + % so the path does not get interrupted + \let\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentlineto + \let\pgf@nfold@cur@endangle\pgf@nfold@next@startangle + \else\if#1e + \pgfpathlineto{\pgf@nfold@arrowintersect} \else - \pgfmathparse{\pgf@shorten@start@additional} - \fi - \pgfpointadd% - {\pgf@decorate@inputsegment@first}% - {\pgfpointpolar{\pgfdecoratedinputsegmentstartangle}{\pgfmathresult pt}} - % - \pgfpathmoveto{} - \pgf@nfold@storedata@handle@segment - \endgroup\bgroup - } - \state{segment}[width=\pgfdecoratedinputsegmentremainingdistance, next state=segment]{% - \egroup\begingroup + \pgfutil@packageerror{tikz-nfold}{Invalid argument to \string\pgf@nfold@extendtotip: \meaning#1}{} + \fi\fi \pgftransformreset - \pgf@nfold@storedata@handle@segment - \endgroup\bgroup - }% - \state{final}{} -}% + \fi\fi% if 1 < i < nArrows + \fi% if intersections is needed and not loaded +} +% Parsing the arrow tips +% ---------------------- +% +% We need to detect if the user has set Implies[] arrows at the start and/or end tip. +% To do so, we parse \pgf@start@tip@sequence. If the user specifies Implies[] manually, +% we find that +% pgf@start@tip@sequence=\pgf@arrow@handle{Implies}{...} +% However, in other cases (like tikz-cd) we may find +% \pgf@arrow@handle@shorthand@empty {\csname pgf@ar@means@tikzcd implies cap\endcsname } +% In such cases we must expand the first parameter once and then match as above. -% Detecting arrow tips -% -------------------- +% Set global defaults +\def\pgf@nfold@start@arrowcode{0} +\def\pgf@nfold@end@arrowcode{0} -% Old arrow matching -% -% This is the content of \pgf@arrow@tip@sequence for arrows | and Implies, respectively -% -%\edef\pgf@arrow@macro@verticalline{\noexpand\pgf@arrow@handle@shorthand@empty {\expandafter\noexpand\csname pgf@ar@means@|\endcsname }} -%\def\pgf@arrow@macro@Implies{\pgf@arrow@handle {Implies}{}} -% The next one is wrong; use \csname pgf@ar@means@tikzcd implies\endcsname -%\def\tikzcd@arrow@implies{\pgf@arrow@handle@shorthand@empty {\pgf@ar@means@tikzcd implies }} - -% Intercept the arrows at definition -\let\pgf@nfold@cachedarrows\pgfutil@empty % default value -\let\pgf@nfold@oldsetarrows\pgfsetarrows -\def\pgfsetarrows#1{% - \def\pgf@nfold@cachedarrows{#1}% - \pgf@nfold@oldsetarrows{#1}% -} - -\let\pgf@nfold@cached@arrow@start\pgfutil@empty -\let\pgf@nfold@cached@arrow@end\pgfutil@empty -\def\pgf@nfold@storearrows{% - \ifx\pgf@nfold@cachedarrows\pgfutil@empty - % reset the cached values in case we don't have an arrow; this is important - % because the cached values are set globally - \global\let\pgf@nfold@cached@arrow@start\pgfutil@empty - \global\let\pgf@nfold@cached@arrow@end\pgfutil@empty - \else% - \expandafter\pgf@nfold@parsearrows@\pgf@nfold@cachedarrows\pgf@stop% - \fi% +\def\pgf@nfold@parsearrows{ + \ifpgfutil@tempswa% this is set in \pgfusepath and stores whether we draw arrow tips at all + \expandafter\pgf@nfold@parsearrowmacro\pgf@start@tip@sequence\relax + \let\pgf@nfold@start@arrowcode\pgf@nfold@detectedarrow + \expandafter\pgf@nfold@parsearrowmacro\pgf@end@tip@sequence\relax + \let\pgf@nfold@end@arrowcode\pgf@nfold@detectedarrow + \else + \def\pgf@nfold@start@arrowcode{0} + \def\pgf@nfold@end@arrowcode{0} + \fi } -\def\pgf@nfold@parsearrows@#1-#2\pgf@stop{% - % These must be set globally because \pgf@nfold@storearrows is called from within a decoration; - % also, this cached value must survive subsequent calls of \pgfsetarrows{} which disable arrows - \gdef\pgf@nfold@cached@arrow@start{#1}% - \gdef\pgf@nfold@cached@arrow@end{#2}% -} - -% Here we match against the names of the arrows that were set in \pgfarrowsset{...}. -% This does not respect aliases, i.e. redefining /tikzcd implies cap will not have -% the desired effect. We may be able to fix this in the future, see below. -\def\pgf@nfold@nonetip@i{tikzcd implies cap} -\def\pgf@nfold@impliestip@i{Implies} -\def\pgf@nfold@impliestip@ii{tikzcd implies} -\def\pgf@nfold@mapstotip@i{|} -\def\pgf@nfold@mapstotip@ii{tikzcd implies bar} -\def\pgf@nfold@mapstotip@iii{Bar} -\newif\ifpgf@nfold@tip@implies -\newif\ifpgf@nfold@tip@mapsto - -\def\pgf@nfold@check@tip@start{% -\pgf@nfold@check@tip{\pgf@nfold@cached@arrow@start}% -} -\def\pgf@nfold@check@tip@end{% -\pgf@nfold@check@tip{\pgf@nfold@cached@arrow@end}% -} - -\def\pgf@nfold@check@tip#1{% - \pgf@nfold@tip@impliesfalse% - \pgf@nfold@tip@mapstofalse% - \ifx#1\pgfutil@empty% - % no tip + +\def\pgf@nfold@parsearrowmacro#1{% + \def\pgf@nfold@detectedarrow{0} + \ifx#1\relax + \let\pgf@next\relax \else - \ifx#1\pgf@nfold@nonetip@i% - % no tip + \ifx#1\pgf@arrow@handle + % found \pgf@arrow@handle{...}, now parse the first parameter + \let\pgf@next\pgf@nfold@parse@arrow@handle \else - \ifx#1\pgf@nfold@impliestip@i% - \pgf@nfold@tip@impliestrue% + \ifx#1\pgf@arrow@handle@shorthand@empty + \let\pgf@next\pgf@nfold@parse@shorthandempty \else - \ifx#1\pgf@nfold@impliestip@ii% - \pgf@nfold@tip@impliestrue% - \else - \ifx#1\pgf@nfold@mapstotip@i% - \pgf@nfold@tip@mapstotrue% - \else - \ifx#1\pgf@nfold@mapstotip@ii% - \pgf@nfold@tip@mapstotrue% - \else - \ifx#1\pgf@nfold@mapstotip@iii% - \pgf@nfold@tip@mapstotrue% - \else - \pgfwarning{'nfold': Unsupported arrow tip "#1"} - \fi - \fi - \fi - \fi + % found nothing + \let\pgf@next\pgfutil@gobble@until@relax \fi \fi \fi + \pgf@next } -% Future: In principle, we can dereference aliases (means=...) the following way: +\def\pgf@nfold@param@Implies{Implies} + +\def\pgf@nfold@parse@arrow@handle#1{% + \def\pgf@tmp{#1} + \ifx\pgf@tmp\pgf@nfold@param@Implies + \def\pgf@nfold@detectedarrow{1} + \fi + \pgfutil@gobble@until@relax +} + +\def\pgf@nfold@parse@shorthandempty#1{ + % Expand #1 once (\pgf@arrow@handle@shorthand@empty is just an identity operator) + \expandafter\def\expandafter\pgf@tmp\expandafter{#1} + \expandafter\pgf@nfold@parsearrowmacro\pgf@tmp\relax + % still need to gobble the rest of the orginal arrow definition + \pgfutil@gobble@until@relax +} + + % -% \ifcsname pgf@ar@means@tikzcd implies bar\endcsname -% \expandafter\let\expandafter\tempmacro\csname pgf@ar@means@tikzcd implies bar\endcsname -% \pgfwarning{\meaning\tempmacro} -% \fi +% Hooking into pgf's rendering pipeline +% ------------------------------------- % -% However, the alias is not a name, but a macro, which in turn could call dereference aliases. -% We could go down this rabbit hole in the future, but I won't do that for now. +% The new code has to be injected into \pgfusepath (pgfcorepathusage.code.tex). For rendering the new paths, +% \pgf@stroke@inner@line is a natural choice as this is where /tikz/double is rendered. However, we also +% need to disable rendering the ordinary path, which is not as easy. In the future I will make a pull request +% to TikZ to simplify such injections. +% +% The call to draw the path comes right before \pgf@stroke@inner@line. The macro before \pgf@stroke@inner@line +% is either \pgf@path@check@proper or \pgf@prepare@start@of@path (depending on the result of the proper check). +% We therefore must inject code into both of them to see if nfold is enabled. If it is, we call the old macro, +% cache and delete the current softpath (so the call to \pgfsyssoftpath@invokecurrentpath has no effect), then we +% restore and offset the cached softpath in \pgf@stroke@inner@line. +% +% The macros \pgf@path@check@proper and \pgf@prepare@start@of@path are also used in \pgf@up@draw@arrows@only, +% so we must make sure that the latter is unaffected by the modifications. Luckily, this turns out not to be +% a problem - the only macros that are called after the modified ones are \pgf@add@arrow@at@start and +% \pgf@add@arrow@at@end, which do not change their behaviour even if we modify the paths. % -% For now this means that redefining /tikz/commutative diagrams/tikzcd implies cap will not -% have the desired effect. +\newcount\pgf@nfold@order +\pgf@nfold@order=1 + +\def\pgf@nfold@preparenfoldpath{% + \ifnum\pgf@nfold@order>1\relax + \ifdim\pgfinnerlinewidth>0pt\relax + % Hack the rendering pipeline: There is a \pgfsyssoftpath@invokecurrentpath call following + % which we do not want if nfold is active. We therefore clear the current path here + % and then perform the nfold drawing in our modification of \pgf@stroke@inner@line + \pgfsyssoftpath@getcurrentpath\cachedpath% + \pgfsyssoftpath@setcurrentpath\pgfutil@empty% + \else + \pgfutil@packageerror{tikz-nfold}{Must set \string\pgfinnerlinewidth\space to use nfold, e.g. by setting /tikz/double distance}{} + \fi + \fi +} + +\let\pgf@nfold@old@path@check@proper\pgf@path@check@proper +\def\pgf@path@check@proper{% + \pgf@nfold@old@path@check@proper% + \ifpgfutil@tempswa\else% + % if \pgfutil@tempswa is false, this is the last macro we can overwrite before the draw call. + % Otherwise, we inject into \pgf@prepare@start@of@path% + \pgf@nfold@preparenfoldpath% + \fi% +} + +\let\pgf@nfold@old@prepare@start@of@path\pgf@prepare@start@of@path +\def\pgf@prepare@start@of@path{% + \pgf@nfold@old@prepare@start@of@path% + \pgf@nfold@preparenfoldpath% +} + +\let\pgf@nfold@old@stroke@inner@line\pgf@stroke@inner@line +\def\pgf@stroke@inner@line{% + \ifnum\pgf@nfold@order>1\relax% + \pgf@nfold@render@cached@softpath% + \else% + % Old behaviour + \pgf@nfold@old@stroke@inner@line% + \fi% +} -% Rendering arrow tips -% -------------------- -% Precomputed intersections % -% For arrows of order n > 2 with an Implies tip, the constituent parts of the n-fold arrow -% end somewhere in the middle of the tip. The exact end point must be computed using -% the intersections library. To speed up compilation times, the intersection points are precomputed -% up to n = 5. If your document contains arrows of order 6 or larger, consider adding those -% as well; the values are output in the log file. -\expandafter\def\csname tikz@arrow@intersec@cache@2@3\endcsname{\pgfqpoint{2pt}{0pt}} -\expandafter\def\csname tikz@arrow@intersec@cache@2@4\endcsname{\pgfqpoint{0.94063pt}{-0.33333pt}} -\expandafter\def\csname tikz@arrow@intersec@cache@3@4\endcsname{\pgfqpoint{0.94063pt}{0.33333pt}} -\expandafter\def\csname tikz@arrow@intersec@cache@2@5\endcsname{\pgfqpoint{0.64167pt}{-0.5pt}} -\expandafter\def\csname tikz@arrow@intersec@cache@3@5\endcsname{\pgfqpoint{2pt}{0pt}} -\expandafter\def\csname tikz@arrow@intersec@cache@4@5\endcsname{\pgfqpoint{0.64167pt}{0.5pt}} -% intersections are precomputed up to this order -\def\tikz@arrow@intersec@numcached{5} - -% Decoration for the start tip -\pgfdeclaredecoration{pgf@nfold@tip@start}{start}{% - \state{start}[width=\pgfdecoratedinputsegmentremainingdistance, next state=segment]{% - \egroup\begingroup% - \pgf@nfold@check@tip@start% - \pgf@nfold@setup@start@tip% - \ifpgf@nfold@tip@implies% - \pgf@nfold@setupimplies% - \pgf@nfold@drawimplies% - \fi% - \ifpgf@nfold@tip@mapsto% - \pgf@nfold@setupmapsto% - \pgf@nfold@drawmapsto% +% Parsing the soft path +% --------------------- +% +% A significant part of the code below is based on pgfmoduledecorations.code.tex (c) 2019 Mark Wibrow and Till Tantau. +% Quite similar to decorations we parse the current soft path and put it into a form that makes it easier +% to iterate over. +% +% + +\def\pgf@nfold@parsesoftpath#1#2{% + \def\pgf@nfold@inputsegmentobjectsmacro{#2}% + \let\pgf@nfold@inputsegmentobjects\pgfutil@empty% + \pgfutil@tempdima0pt\relax% + \edef\pgfpoint@nfold@lastparsed{\pgf@x\the\pgf@path@lastx\pgf@y\the\pgf@path@lasty}% + \let\pgfpoint@nfold@lastnonmovetoparsed\pgfpoint@origin% + \let\pgf@nfold@queueinputsegmentobject\pgfutil@empty% + \let\pgfpoint@nfoldd@firstparsed\pgfutil@empty% + \expandafter\pgf@nfold@@parsesoftpath#1\pgf@stop% +}% + +\def\pgf@nfold@@parsesoftpath#1{% + \ifx#1\pgf@stop% + \let\pgf@nfold@queueinputsegmentobject\pgfutil@empty% <- removes final moveto (may not be desirable). + \pgf@nfold@addtoinputsegmentobjects{\pgf@nfold@inputsegmentobject@endofinputsegments}% + % probably so that the last segment also has a well-defined next segment + \pgf@nfold@addtoinputsegmentobjects{\pgf@nfold@inputsegmentobject@endofinputsegments}% + \expandafter\let\pgf@nfold@inputsegmentobjectsmacro\pgf@nfold@inputsegmentobjects% + \let\pgf@next\relax% + \else% + \ifx#1\pgfsyssoftpath@movetotoken% + \let\pgf@next\pgf@nfold@parsemoveto% + \else% + \ifx#1\pgfsyssoftpath@linetotoken% + \let\pgf@next\pgf@nfold@parselineto% + \else% + \ifx#1\pgfsyssoftpath@curvetosupportatoken% + \let\pgf@next\pgf@nfold@parsecurveto% + \else% + \ifx#1\pgfsyssoftpath@closepathtoken% + \let\pgf@next\pgf@nfold@parseclosepath% + \else% + \ifx#1\pgfsyssoftpath@rectcornertoken% + \let\pgf@next\pgf@nfold@parserect% + \else% + \pgfutil@packageerror{tikz-nfold}{Unrecognised soft path token `#1'}{}% + \fi% + \fi% + \fi% + \fi% \fi% - \endgroup\bgroup% + \fi% + \pgf@next}% + +\def\pgf@nfold@parsemoveto#1#2{% + \def\pgf@nfold@queueinputsegmentobject{\pgf@nfold@inputsegmentobject@moveto{\pgf@x#1\pgf@y#2}}% + \def\pgfpoint@nfold@lastparsed{\pgf@x#1\pgf@y#2}% + \pgf@nfold@@parsesoftpath% +}% + +% Convert \pgfsyssoftpath@linetotoken{<X>}{<Y>} into the following +% representation: +% +% \pgf@nfold@inputsegmentobject@lineto{<length>}{\pgf@x <Last X> \pgf@y <Last Y>}{\pgf@x <X> \pgf@y <Y>} +% +\def\pgf@nfold@parselineto#1#2{% + % remove degenerate line segments (reduces glitches) + \pgf@process{\pgfpointdiff{\pgfpoint@nfold@lastparsed}{\pgf@x#1\pgf@y#2}} + \pgfpointtaxicabnorm\pgf@xa + \ifdim\pgf@xa>.1pt\relax + \edef\pgf@nfold@temp{% + \noexpand\pgf@nfold@inputsegmentobject@lineto{\pgfpoint@nfold@lastparsed}{\pgf@x#1\pgf@y#2}% + }% + \edef\pgfpoint@nfold@lastparsed{\pgf@x#1\pgf@y#2}% + \let\pgfpoint@nfold@lastnonmovetoparsed\pgfpoint@nfold@lastparsed% + \expandafter\pgf@nfold@addtoinputsegmentobjects\expandafter{\pgf@nfold@temp}% + \fi + \pgf@nfold@@parsesoftpath% +}% + +\def\pgf@nfold@parsecurveto#1#2\pgfsyssoftpath@curvetosupportbtoken#3#4\pgfsyssoftpath@curvetotoken#5#6{% + \edef\pgf@nfold@temp{% + \noexpand\pgf@nfold@inputsegmentobject@curveto{\pgfpoint@nfold@lastparsed}% + {\pgf@x#1\pgf@y#2}{\pgf@x#3\pgf@y#4}{\pgf@x#5\pgf@y#6}% }% - \state{segment}[width=\pgfdecoratedinputsegmentremainingdistance, next state=segment]{}% + \expandafter\pgf@nfold@addtoinputsegmentobjects\expandafter{\pgf@nfold@temp}% + \edef\pgfpoint@nfold@lastparsed{\pgf@x#5\pgf@y#6}% + \let\pgfpoint@nfold@lastnonmovetoparsed\pgfpoint@nfold@lastparsed% + \pgf@nfold@@parsesoftpath% }% -% Decoration for the end tip -\pgfdeclaredecoration{pgf@nfold@tip@end}{segment}{% - \state{segment}[width=\pgfdecoratedinputsegmentremainingdistance, next state=segment]{} - \state{final}{% - \egroup\begingroup% - \pgf@nfold@check@tip@end% this step already checks for unsupported arrows - \pgf@nfold@setup@end@tip - \ifpgf@nfold@tip@implies% - \pgf@nfold@setupimplies% - \pgf@nfold@drawimplies% - \fi% - \ifpgf@nfold@tip@mapsto - \pgf@nfold@setupmapsto - \pgf@nfold@drawmapsto - \fi - \endgroup\bgroup% - } +\def\pgf@nfold@parseclosepath#1#2{% + \edef\pgf@nfold@temp{% + \noexpand\pgf@nfold@inputsegmentobject@closepath{\pgfpoint@nfold@lastparsed}{\pgf@x#1\pgf@y#2}% + }% + \let\pgfpoint@nfold@lastnonmovetoparsed\pgfpoint@nfold@lastparsed% + \expandafter\pgf@nfold@addtoinputsegmentobjects\expandafter{\pgf@nfold@temp}% + \pgf@nfold@@parsesoftpath% }% +% Mostly for the sake of completeness; using TikZ' "\path (0,0) rectangle (1,1);" does not call this code +\def\pgf@nfold@parserect#1#2\pgfsyssoftpath@rectsizetoken#3#4{% + \let\pgf@nfold@orig@@parsesoftpath\pgf@nfold@@parsesoftpath% + \let\pgf@nfold@@parsesoftpath\relax% + \pgf@nfold@parsemoveto{#1}{#2}% + \pgf@xb=#1\relax + \pgf@yb=#2\relax + \pgf@xc=#3\relax + \pgf@yc=#4\relax + \advance\pgf@yb\pgf@yc% + \edef\pgf@temp{{\the\pgf@xb}{\the\pgf@yb}}% + \expandafter\pgf@nfold@parselineto\pgf@temp% + \advance\pgf@xb\pgf@xc% + \edef\pgf@temp{{\the\pgf@xb}{\the\pgf@yb}}% + \expandafter\pgf@nfold@parselineto\pgf@temp% + \advance\pgf@yb-\pgf@yc% + \edef\pgf@temp{{\the\pgf@xb}{\the\pgf@yb}}% + \expandafter\pgf@nfold@parselineto\pgf@temp% + \advance\pgf@xb-\pgf@xc% + \edef\pgf@temp{{\the\pgf@xb}{\the\pgf@yb}}% + \expandafter\pgf@nfold@parselineto\pgf@temp% + \let\pgf@nfold@@parsesoftpath\pgf@nfold@orig@@parsesoftpath% + \edef\pgf@marshal{\noexpand\pgf@nfold@parsemoveto{\the\pgf@xb}{\the\pgf@yb}}% + \pgf@marshal% +} + +\def\pgf@nfold@addtoinputsegmentobjects#1{% + % + % If there is an input segment object waiting (i.e. a moveto), insert it here. + % + \ifx\pgf@nfold@queueinputsegmentobject\pgfutil@empty% + \else% + \let\pgf@nfold@temp\pgf@nfold@queueinputsegmentobject% + \let\pgf@nfold@queueinputsegmentobject\pgfutil@empty% + \expandafter\pgf@nfold@addtoinputsegmentobjects\expandafter{\pgf@nfold@temp}% + \fi% + \ifx\pgfpoint@nfold@firstparsed\pgfutil@empty% + #1% + \let\pgfpoint@nfold@firstparsed\pgf@nfold@inputsegment@first% + \fi% + \expandafter\def\expandafter\pgf@nfold@inputsegmentobjects\expandafter% + {\pgf@nfold@inputsegmentobjects{#1}}% +}% + + +\def\pgf@nfold@inputsegmentfirst{first}% +\def\pgf@nfold@inputsegmentmoveto{moveto}% +\def\pgf@nfold@inputsegmentlineto{lineto}% +\def\pgf@nfold@inputsegmentcurveto{curveto}% +\def\pgf@nfold@inputsegmentclosepath{closepath}% +\def\pgf@nfold@inputsegmentlast{last}% + + +\def\pgf@nfold@inputsegmentobject@moveto#1{% + \def\pgf@nfold@inputsegment@first{#1}% + \def\pgf@nfold@inputsegment@supporta{#1}% + \def\pgf@nfold@inputsegment@supportb{#1}% + \def\pgf@nfold@inputsegment@last{#1}% + \edef\pgf@nfold@lastmoveto{#1}% + \let\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentmoveto% +}% + +\def\pgf@nfold@inputsegmentobject@lineto#1#2{% + \def\pgf@nfold@inputsegment@first{#1}% + \def\pgf@nfold@inputsegment@last{#2}% + \let\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentlineto% +}% -\def\pgf@nfold@setup@start@tip{ +\def\pgf@nfold@inputsegmentobject@curveto#1#2#3#4{% + \def\pgf@nfold@inputsegment@first{#1}% + \def\pgf@nfold@inputsegment@supporta{#2}% + \def\pgf@nfold@inputsegment@supportb{#3}% + \def\pgf@nfold@inputsegment@last{#4}% + \let\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentcurveto% +}% + +\def\pgf@nfold@inputsegmentobject@closepath#1#2{% + \def\pgf@nfold@inputsegment@first{#1}% + \def\pgf@nfold@inputsegment@last{#2}% + \let\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentclosepath% +}% + +\def\pgf@nfold@inputsegmentobject@endofinputsegments{% + \let\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentlast% +}% + + +\def\pgf@nfold@traversepath{% + % Transformations are already baked into the path; without this call, they would be applied twice \pgftransformreset - \pgftransformshift{\pgf@nfold@original@first} - \pgftransformrotate{\pgfdecoratedinputsegmentstartangle} - \pgftransformshift{\pgfpoint{.5*\pgflinewidth}{0pt}} - \pgftransformscale{\pgf@decoration@nfold@hwidth} - \pgftransformxscale{-1.} + % could likely also use \let\pgf@nfold@currentinputsegmentobjects\parsedsoftpath here + \let\pgf@nfold@currentinputsegmentobjects\pgf@nfold@inputsegmentobjects% +% \let\pgf@nfold@transformtoinputsegment\pgfutil@empty% we may need this one for closepath + \pgf@nfold@getnextinputsegmentobject\pgf@nfold@nextinputsegmentobject% + \let\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentfirst% + \def\pgf@nfold@cur@endangle{0.0}% put in some default value so \pgf@nfold@previous@endangle is not undefined + \pgf@nfold@traversepath@ } -\def\pgf@nfold@setup@end@tip{ - \pgftransformreset - \pgftransformshift{\pgf@nfold@original@last} - \pgftransformrotate{\pgf@nfold@cached@endangle} - \pgftransformshift{\pgfpoint{-.5*\pgflinewidth}{0pt}} - \pgftransformscale{\pgf@decoration@nfold@hwidth} + + +\def\pgf@nfold@traversepath@{ + \pgf@nfold@processnextinputsegmentobject% + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentlast\else + \pgf@nfold@handlesegment% + \fi% + \ifx\pgf@nfold@currentinputsegmentobjects\pgfutil@empty% + \let\pgf@next\relax% + \else% + \let\pgf@next\pgf@nfold@traversepath@% + \fi% + \pgf@next% } -\def\pgf@nfold@setupimplies{% - \pgfsetroundjoin% - \pgfsetroundcap% + +\def\pgf@nfold@computeangles{ + \ifx\pgf@nfold@currentinputsegment\pgf@nfold@inputsegmentcurveto% + \pgf@offset@compute@tangents{\pgf@nfold@inputsegment@first}{\pgf@nfold@inputsegment@supporta}{\pgf@nfold@inputsegment@supportb}{\pgf@nfold@inputsegment@last} + \let\pgf@nfold@cur@startangle\pgf@tmp@angle@i + \let\pgf@nfold@cur@endangle\pgf@tmp@angle@ii + \else% + \pgfmathanglebetweenpoints{\pgf@nfold@inputsegment@first}{\pgf@nfold@inputsegment@last}% + \edef\pgf@nfold@cur@startangle{\pgfmathresult} + \edef\pgf@nfold@cur@endangle{\pgfmathresult} + \fi% } -\def\pgf@nfold@drawimplies{ - \pgftransformshift{\pgfqpoint{-2pt}{0pt}} - \pgfpathmoveto{\pgfqpoint{-1.4pt}{2.65pt}} - \pgfpathcurveto{\pgfqpoint{-0.75pt}{1.25pt}}{\pgfqpoint{1pt}{0.05pt}}{\pgfqpoint{2pt}{0pt}} - \pgfpathcurveto{\pgfqpoint{1pt}{-0.05pt}}{\pgfqpoint{-0.75pt}{-1.25pt}}{\pgfqpoint{-1.4pt}{-2.65pt}} + +\def\pgf@nfold@clampangle{ + % The computed angles are values between 0 and 360, so their difference is between 360 and -360; + % we want the difference to be between -180 and 180 + \ifdim\pgfmathresult pt<-180pt\relax + \pgfutil@tempdima=\pgfmathresult pt + \advance\pgfutil@tempdima by 360pt + \edef\pgfmathresult{\pgf@sys@tonumber\pgfutil@tempdima} + \else\ifdim\pgfmathresult pt>180pt\relax + \pgfutil@tempdima=\pgfmathresult pt + \advance\pgfutil@tempdima by -360pt + \edef\pgfmathresult{\pgf@sys@tonumber\pgfutil@tempdima} + \fi\fi } -\def\pgf@nfold@setupmapsto{% - \pgfsetbuttcap% + +\def\pgf@nfold@getnextinputsegmentobject#1{% + \ifx\pgf@nfold@currentinputsegmentobjects\pgfutil@empty% + \let\pgf@next\relax% + \else% + \def\pgf@nfold@temp{#1}% + \let\pgf@next\pgf@nfold@@getnextinputsegmentobject% + \fi% + \pgf@next% +}% +\def\pgf@nfold@@getnextinputsegmentobject{% + \expandafter\pgf@nfold@@@getnextinputsegmentobject\pgf@nfold@currentinputsegmentobjects\pgf@stop}% +\def\pgf@nfold@@@getnextinputsegmentobject#1#2\pgf@stop{% + \expandafter\def\pgf@nfold@temp{#1}% + \def\pgf@nfold@currentinputsegmentobjects{#2}}% + +\def\pgf@nfold@processnextinputsegmentobject{% + \let\pgf@nfold@previousinputsegment\pgf@nfold@currentinputsegment% + \let\pgf@nfold@previous@endangle\pgf@nfold@cur@endangle% + \let\pgf@nfold@currentinputsegmentobject\pgf@nfold@nextinputsegmentobject% + \pgf@nfold@getnextinputsegmentobject\pgf@nfold@nextinputsegmentobject% + \pgf@nfold@nextinputsegmentobject% parse the *next* input segment so we can compute its angles + \pgf@nfold@computeangles% + \let\pgf@nfold@next@first\pgf@nfold@inputsegment@first% + \let\pgf@nfold@next@supporta\pgf@nfold@inputsegment@supporta% + \let\pgf@nfold@next@supportb\pgf@nfold@inputsegment@supportb% + \let\pgf@nfold@next@last\pgf@nfold@inputsegment@last% + \let\pgf@nfold@next@segmenttype\pgf@nfold@currentinputsegment% + \let\pgf@nfold@next@startangle\pgf@nfold@cur@startangle% + \pgf@nfold@currentinputsegmentobject% get the current segment into \pgf@nfold@inputsegment@first etc. + \pgf@nfold@computeangles% + % Compute the angle differences at the start and end (between -180 and +180 degrees) + % using \pgfmathsubtract@ is more readable and no less efficient than computing this manually + \pgfmathsubtract@{\pgf@nfold@cur@startangle}{\pgf@nfold@previous@endangle} + \pgf@nfold@clampangle + \edef\pgf@nfold@deltaphi@start{\pgfmathresult} + \pgfmathsubtract@{\pgf@nfold@next@startangle}{\pgf@nfold@cur@endangle} + \pgf@nfold@clampangle + \edef\pgf@nfold@deltaphi@end{\pgfmathresult} } -\def\pgf@nfold@drawmapsto{ - % Try: use the same width as the arrow head, minus the caps - \pgfpathmoveto{\pgfqpoint{0pt}{2.65pt}} - \pgfpathlineto{\pgfqpoint{0pt}{-2.65pt}} + + +% +% Iterating over the parsed soft path +% ----------------------------------- +% + +\newcount\pgf@nfold@index +\def\pgf@nfold@run@loop{% + \pgf@nfold@index=\pgf@nfold@order% + \pgf@nfold@run@loop@% } -% This macro extends the arrow body to the tips -% parameter: s=start, e=end -\def\pgf@nfold@extendtotip#1{ - % Do not extend the arrow for index=1 and index=order, it already ends in the right place - \ifnum\pgf@nfold@index>1\ifnum\pgf@nfold@index<\pgf@nfold@dec@order\relax% - % Step 1: Find the intersection of the arrow's path with the head. This is is computationally - % expensive, so we first look if the value has been precomputed. - \ifcsname tikz@arrow@intersec@cache@\pgf@nfold@index @\the\pgf@nfold@dec@order\endcsname - \csname tikz@arrow@intersec@cache@\pgf@nfold@index @\the\pgf@nfold@dec@order\endcsname - \pgfstorepoint\pgf@nfold@arrowintersect - \else - % the intersection has not been precomputed, thus compute on the fly here - \pgfintersectionofpaths{ - % specify the tip - \pgf@nfold@drawimplies - }{ - % extend the body to intersect the tip - \pgfpathmoveto{\pgfqpoint{-3pt}{\pgf@shiftdec@fraction pt}} - \pgfpathlineto{\pgfqpoint{3pt}{\pgf@shiftdec@fraction pt}} - } - \ifnum\pgfintersectionsolutions>0 - \pgfpointintersectionsolution{1} - \pgfstorepoint\pgf@nfold@arrowintersect - \makeatother - \typeout{tikz-nfold: computed intersection cache@\pgf@nfold@index @\the\pgf@nfold@dec@order: \string\pgfqpoint{\the\pgf@x}{\the\pgf@y}^^J} - \makeatletter - \else - % this is a failsafe and should never be reached - \pgfwarning{'nfold': did not find intersection} - \pgfqpoint{0pt}{\pgf@shiftdec@fraction pt} - \pgfstorepoint\pgf@nfold@arrowintersect - \fi - \fi% if precomputed - % Step 2: Extend the arrow body to the intersection point. - % If the tip is at the beginning of the path, we have to move to the intersection - % and then draw a line to the "regular" starting point. The subsequent segment then - % should omit its moveto. - % If the tip is at the end, we are already in the right position and only need to extend - % the current path to the intersection point. - \pgftransformreset - \if#1s - \pgftransformshift{\pgf@nfold@original@first} - \pgftransformrotate{\pgfdecoratedinputsegmentstartangle} - \pgftransformxscale{-1} - \else - \pgftransformshift{\pgf@nfold@original@last} - \pgftransformrotate{\pgfdecoratedinputsegmentendangle} - \fi - \pgftransformshift{\pgfpoint{-2*\pgf@decoration@nfold@hwidth-.5*\pgflinewidth}{0pt}} - \pgftransformscale{\pgf@decoration@nfold@hwidth} - \if#1s - \pgfpathmoveto{\pgf@nfold@arrowintersect} - % This is precisely the start of the body, shifted vertically - \pgfpathlineto{\pgfqpoint{-0.06pt}{\pgf@shiftdec@fraction pt}} - \pgf@nfold@continuesegmenttrue - \else\if#1e - \pgfpathlineto{\pgf@nfold@arrowintersect} - \else - \pgferror{Invalid argument to \string\pgf@nfold@extendtotip: \meaning#1} - \fi\fi - \pgftransformreset -\fi\fi% if 1 < i < nArrows +\def\pgf@nfold@run@loop@{% + \pgf@nfold@loop@inner% + \advance\pgf@nfold@index by -1\relax + \ifnum\pgf@nfold@index>0\relax% + \pgf@nfold@run@loop@% + \fi% +} + +\def\pgf@nfold@loop@inner{% + \pgfmathsetmacro{\pgf@nfold@shift@fraction}% + {-1+2*(\pgf@nfold@index-1)/(\pgf@nfold@order-1)} + \pgfmathsetlengthmacro{\pgf@nfold@shiftamount}{\pgf@nfold@hwidth*\pgf@nfold@shift@fraction} + \pgf@nfold@traversepath% + \pgfsyssoftpath@flushcurrentpath% + \pgf@up@action% +} + +% Computes both the width of the component lines into \pgf@x and the distance +% from the center to the outermost line centers into \pgf@y +% from the current values of \pgflinewidth and \pgfinnerlinewidth. +\def\pgf@nfold@compute@widths@from@double{ + \pgf@x=\pgflinewidth\relax% + \pgf@y=\pgf@x\relax% + \advance\pgf@x-\pgfinnerlinewidth\relax% + \advance\pgf@y+\pgfinnerlinewidth\relax% + \pgf@x=.5\pgf@x\relax% + \pgf@y=.25\pgf@y\relax% +} + +\def\pgf@nfold@render@cached@softpath{% + \pgfscope% must use a scope, otherwise we break the arrow tips + \pgfprocessround{\cachedpath}{\cachedpath}% remove tokens from the soft path + \pgf@nfold@parsesoftpath{\cachedpath}{\parsedsoftpath} + \pgf@nfold@parsearrows + % Compute the full and constituent part line widths + \pgf@nfold@compute@widths@from@double% + \pgfsetlinewidth\pgf@x% + \edef\pgf@nfold@hwidth{\the\pgf@y} + \pgf@nfold@run@loop + \endpgfscope } % -% n-fold decoration -% ----------------- +% user interface and pgf/TikZ keys +% -------------------------------- % -% This decoration is intended to be used by the end user. It replaces a given path by n parallel versions of the path. - -\newcount\tikz@nfold@loop@index -\def\tikz@nfold@run@loop#1{% - \edef\tikz@nfold@postaction{% - \tikz@nfold@postaction% this already includes a comma - postaction={% - draw,arrows=-,shorten <=0pt,shorten >=0pt,% disable shorten, is implemented in the pre-pass - decorate,decoration={% - pgf@nfold@shift, nfold order=#1, nfold index=\the\tikz@nfold@loop@index% - }% - },% - } - \advance\tikz@nfold@loop@index by -1\relax - \ifnum\tikz@nfold@loop@index>0% - \tikz@nfold@run@loop{#1}% - \fi% + +% Outputs a provided soft path in #1 offset by a distance provided in #2. +\def\pgfoffsetpath#1#2{% + \begingroup + \pgfmathsetlengthmacro\pgf@nfold@hwidth{#2} + % \pgf@nfold@hwidth must always be positive + \pgf@x=\pgf@nfold@hwidth\relax + \ifdim\pgf@x<0pt\relax + \pgf@x=-\pgf@x + \def\pgf@nfold@shift@fraction{-1} + \else + \def\pgf@nfold@shift@fraction{1} + \fi + \edef\pgf@nfold@hwidth{\the\pgf@x} + \pgfoffsetpathqfraction{#1}{\pgf@nfold@hwidth}{\pgf@nfold@shift@fraction} + \endgroup +} + +% Outputs a provided soft path in #1 offset by #3*#2 where #2 is a length (>= 0 pt) +% and #3 is a number between -1.0 and 1.0. This differs from \pgfoffsetpath{#1}{#2*#3} +% in how the joins between segments are rendered. In particular, \pgfoffsetpathfraction{#1}{10pt}{0} +% does *not* yield the original path, but a new path in the centre of #1 drawn at line width 20pt. +% +\def\pgfoffsetpathfraction#1#2#3{% + \begingroup + \pgfmathsetlengthmacro\pgf@nfold@hwidth{#2} + \pgfmathsetmacro\pgf@nfold@shift@fraction{#3} + \pgfoffsetpathqfraction{#1}{\pgf@nfold@hwidth}{\pgf@nfold@shift@fraction} + \endgroup +} + +% This has the same output as the #3-th segment of nfold=#4. +\def\pgfoffsetpathindex#1#2#3#4{% + \begingroup + \pgfmathsetmacro\pgf@nfold@shift@fraction{-1+2*(#3-1)/(#4-1)} + \pgfoffsetpathqfraction{#1}{#2}{\pgf@nfold@shift@fraction} + \endgroup +} + +% A quick version that skips processing the input values +\def\pgfoffsetpathqfraction#1#2#3{% + \begingroup + \pgfprocessround{#1}{\cachedpath}% remove tokens from the soft path + \pgf@nfold@parsesoftpath{\cachedpath}{\parsedsoftpath} + \pgf@x=#2\relax + \edef\pgf@nfold@hwidth{\the\pgf@x} + \edef\pgf@nfold@shift@fraction{#3} + \pgf@x=\pgf@nfold@shift@fraction\pgf@x\relax + \edef\pgf@nfold@shiftamount{\the\pgf@x} + \pgf@nfold@traversepath% + \endgroup +} + + +\pgfkeys{ + /pgf/nfold/.code={% + \pgf@nfold@order=#1\relax% + \ifnum\pgf@nfold@order<1\relax% + \pgfutil@packageerror{tikz-nfold}{The key /pgf/nfold must take a value of at least 1, got \the\pgf@nfold@order}{}% + \fi% + % If nfold > numcached AND intersections is not loaded AND we draw an Implies tip, we get an error. + % We check the first two conditions now and set the respective flag + \ifnum\pgf@nfold@order>\pgf@nfold@intersec@numcached\relax + \ifdefined\pgfintersectionofpaths\else + \pgf@nfold@intersectionsnotloadedtrue + \fi + \fi + }, + /pgf/nfold/.default=2 } + + +% use \tikzset for scoping reasons, does not appear to be equivalent to \pgfset{/tikz/...=...} \tikzset{ nfold/.code={ - \tikzset{draw=none} - \pgf@nfold@dec@order=#1\relax - \ifnum\pgf@nfold@dec@order<2 - \pgferror{'nfold' must take a value of at least 2, got \the\pgf@nfold@dec@order} - \fi - \tikz@nfold@loop@index=\pgf@nfold@dec@order - % The final style has the following form: - % [draw=none, - % postaction={ - % draw=none,decorate,decoration=pgf@nfold@storedata, - % postaction={draw,arrows=-,decorate,decoration={pgf@nfold@shift,nfold order=#1,nfold index=1}}, - % [...], - % postaction={draw,arrows=-,decorate,decoration={pgf@nfold@shift,nfold order=#1,nfold index=#1}} - % } - % ] - % We build the postactions piece by piece in the macro \tikz@nfold@postaction. Then we apply it - % using \tikzset and .expand once, so potential future changes to \tikz@nfold@postaction do not - % affect the settings in tikz. - % - \def\tikz@nfold@postaction{} - \tikz@nfold@run@loop{#1} - \edef\tikz@nfold@postaction{% - draw=none,decorate,decoration=pgf@nfold@storedata,% - \tikz@nfold@postaction% - postaction={ - draw,arrows=-,shorten <=0pt,shorten >=0pt, - decorate,decoration=pgf@nfold@tip@start},% - postaction={ - draw,arrows=-,shorten <=0pt,shorten >=0pt, - decorate,decoration=pgf@nfold@tip@end},% + \edef\pgf@tmp{\noexpand\pgfkeys{/pgf/nfold=#1}} + % patch \tikz@double@setup to set /pgf/nfold=#1 as well + \expandafter\expandafter\expandafter\def% + \expandafter\expandafter\expandafter\tikz@double@setup% + \expandafter\expandafter\expandafter{\expandafter\tikz@double@setup\pgf@tmp} + }, + nfold/.default=2, + scaling nfold/.code={% + \pgfscope% scope to contain \tikz@double@setup + \tikz@double@setup + % extract double distance between line centers into \pgf@x + \pgf@nfold@compute@widths@from@double + \pgf@y=2\pgf@y + % store (order-1)*\pgf@x in \pgf@xa + \c@pgf@counta=#1 + \advance\c@pgf@counta by -1\relax + \global\pgf@y=\c@pgf@counta\pgf@y + \endpgfscope + \tikzset{ + double distance between line centers=\pgf@y, + nfold=#1 + } + }, + scaling nfold/.default=2, + % This simply defines the key if tikzcd is not loaded, so we don't run into any errors + commutative diagrams/scaling nfold/.code={ + \pgfscope% scope to contain \tikz@double@setup + \tikz@double@setup + % extract double distance between line centers into \pgf@x + \pgf@nfold@compute@widths@from@double + % store (order-1)*\pgf@y in \pgf@ya + \c@pgf@counta=#1 + \advance\c@pgf@counta by -1\relax + \pgf@ya=\c@pgf@counta\pgf@y + % compute the label offset, which is (order-2)*\pgf@y + .5*\pgf@x + \advance\c@pgf@counta by -1\relax + \pgf@xa=\c@pgf@counta\pgf@y + \advance\pgf@xa by .5\pgf@x + % save the results in \pgf@x and \pgf@y + \global\pgf@x=\pgf@xa + \global\pgf@y=2\pgf@ya + \endpgfscope + \tikzset{ + commutative diagrams/every label/.append style/.expanded={outer sep=\the\pgf@x}, + double distance between line centers=\pgf@y, + nfold=#1 } - \tikzset{postaction/.expand once=\tikz@nfold@postaction} }, - nfold/.default=2 + commutative diagrams/scaling nfold/.default=2 } \endinput |