pgf-tikz / pgf

A Portable Graphic Format for TeX
https://pgf-tikz.github.io/
1.08k stars 104 forks source link

intersections, very slow #1256

Open juanfal opened 1 year ago

juanfal commented 1 year ago

Brief outline of the bug

I've filed the source in here: slow intersection, in essence, I wanted to intersect circles to learnt to command them, it is by far the most adventurous function in TikZ!, but it has turned out so slow, it's is disappointing. And something funny, its speed depends on the size of the figure 10cm is quite more slow than 1cm, I could understand it does an analysis of the whole area, but in both cases it is nearly unusable slow.

Minimal working example (MWE)

\documentclass[tikz,border=10pt]{standalone}
\usetikzlibrary{calc,decorations.markings,intersections,fpu,angles,quotes}
\begin{document}

\begin{tikzpicture}
    \def\sz{3}
    \clip circle (\sz);
    \draw[name path=cen]     circle (\sz);
    \draw[name path=pri] (\sz,0)
        node[left] {1}       circle (\sz);
    \draw[name intersections={of=pri and cen},name path=sec]
        (intersection-1)
        node[below left] {2} circle (\sz);
    \draw[name intersections={of=sec and cen},name path=ter]
        (intersection-1)
        node[below right] {3} circle (\sz);
    \draw[name intersections={of=ter and cen},name path=cua]
        (intersection-1)
        node[right] {4}       circle (\sz);
    \draw[name intersections={of=cua and cen},name path=qui]
        (intersection-2)
        node[above right] {5} circle (\sz);
    \draw[name intersections={of=qui and cen},name path=sex]
        (intersection-2)
        node[above left] {6}  circle (\sz);

\end{tikzpicture}

\end{document}
muzimuzhi commented 1 year ago

Hmm the efficiency of \pgfintersectionofpaths, which is used internally by name intersections option, is sensitive to input paths. \pgfpointintersectionofcircles is super fast, but unfortunately not used by \pgfintersectionofpaths.

Full example

```tex \documentclass{article} \usepackage[margin=.5in, landscape]{geometry} \usepackage{tikz} \usetikzlibrary{intersections} \usepackage{l3benchmark} \ExplSyntaxOn \cs_set_eq:NN \BenchmarkTic \benchmark_tic: \cs_set_eq:NN \BenchmarkToc \benchmark_toc: \ExplSyntaxOff % \pgfIntersectionOfPaths{}{} \newcommand{\pgfIntersectionOfPaths}[2]{ \begin{pgfpicture} \BenchmarkTic \pgfintersectionofpaths { #1 \pgfgetpath{\temppath} \pgfusepath{stroke} \pgfsetpath{\temppath} } { #2 \pgfgetpath{\temppath} \pgfusepath{stroke} \pgfsetpath{\temppath} } \BenchmarkToc \foreach \s in {1,...,\pgfintersectionsolutions} {\pgfpathcircle{\pgfpointintersectionsolution{\s}}{2pt}} \pgfusepath{stroke} \end{pgfpicture} } % only the first intersection is marked % \pgfPointIntersectionOfCircles{}{}{}{} \newcommand{\pgfPointIntersectionOfCircles}[4]{ \begin{pgfpicture} \pgfpathcircle{#1}{#3} \pgfpathcircle{#2}{#4} \pgfpathcircle { \BenchmarkTic \pgfpointintersectionofcircles{#1}{#2}{#3}{#4}{1} \BenchmarkToc } {2pt} \pgfusepath{stroke} \end{pgfpicture} } \begin{document} \typeout{\string\pgfintersectionofpaths, radius=1cm} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{0}{1cm}}{1cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{60}{1cm}}{1cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{120}{1cm}}{1cm}} % SLOW, take 15s+ \typeout{\string\pgfintersectionofpaths, radius=3cm} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{0}{3cm}}{3cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{60}{3cm}}{3cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{120}{3cm}}{3cm}} \typeout{\string\pgfpointintersectionofcircles, radius=1cm} \pgfPointIntersectionOfCircles {\pgfpointorigin}{\pgfpointpolar{0}{1cm}}{1cm}{1cm} \pgfPointIntersectionOfCircles {\pgfpointorigin}{\pgfpointpolar{60}{1cm}}{1cm}{1cm} \pgfPointIntersectionOfCircles {\pgfpointorigin}{\pgfpointpolar{120}{1cm}}{1cm}{1cm} \typeout{\string\pgfpointintersectionofcircles, radius=3cm} \pgfPointIntersectionOfCircles {\pgfpointorigin}{\pgfpointpolar{0}{3cm}}{3cm}{3cm} \pgfPointIntersectionOfCircles {\pgfpointorigin}{\pgfpointpolar{60}{3cm}}{3cm}{3cm} \pgfPointIntersectionOfCircles {\pgfpointorigin}{\pgfpointpolar{120}{3cm}}{3cm}{3cm} \end{document} ```

\pgfintersectionofpaths, radius=1cm
(l3benchmark) + TIC
(l3benchmark) + TOC: 0.222 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 1.37 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 2.86 s
\pgfintersectionofpaths, radius=3cm
(l3benchmark) + TIC
(l3benchmark) + TOC: 0.264 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 4.92 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 11.3 s
\pgfpointintersectionofcircles, radius=1cm
(l3benchmark) + TIC
(l3benchmark) + TOC: 6.71e-4 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 6.41e-4 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 6.41e-4 s
\pgfpointintersectionofcircles, radius=3cm
(l3benchmark) + TIC
(l3benchmark) + TOC: 6.56e-4 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 6.56e-4 s
(l3benchmark) + TIC
(l3benchmark) + TOC: 6.56e-4 s

Output of one group of three examples, \pgfpointintersectionofcircles examples have only one intersection marked image

muzimuzhi commented 1 year ago

\pgfpointintersectionofcircles is super fast, but unfortunately not used by \pgfintersectionofpaths.

\pgfpointintersectionofcircles is only used by the now deprecated (on the manual level) intersection coordinate system. It's introduced in 0ae3a685b2ab87050ee1c418e13c19228b273466, deprecated and replaced by intersections library in https://github.com/pgf-tikz/pgf/commit/3dba4acd1ced8625a41ab4af1e59f56a36f0933c. The last trace of it in pgfmanual is in code example on page of Part III TikZ ist kein Zeichenprogramm.

What \pgfintersectionofpaths gets are two soft paths. With coordinate transformations already applied, it's impossible to detect if one soft path contains a circle (currently made by four curve-to segments).

Original example drawn with \pgfpointintersectionofcircles, note the rounding errors spot at the right margin of the first circle

```tex \documentclass{article} \usepackage{tikz} \usetikzlibrary{calc} \begin{document} \begin{tikzpicture}[mycircle/.style={draw, shape=circle, minimum size=2*\sz cm}] \def\sz{3} \clip circle (\sz); \node[mycircle] (cen) {}; \node[mycircle] (pri) at (\sz, 0) {}; \path[nodes={draw, shape=circle, minimum size=2*\sz cm}] node (sec) at (intersection cs:first node=pri, second node=cen) {} node (ter) at (intersection cs:first node=sec, second node=cen) {} node (cua) at (intersection cs:first node=ter, second node=cen) {} node (qui) at (intersection cs:first node=cua, second node=cen) {} node (sex) at (intersection cs:first node=qui, second node=cen) {}; \end{tikzpicture} \end{document} ``` ![image](https://user-images.githubusercontent.com/6376638/233741508-1b5f5374-c116-4f7a-a220-8f1d15c68da0.png)

juanfal commented 1 year ago

Thanks a lot @muzimuzhi

muzimuzhi commented 1 year ago

With patch (code indent is unaltered on purpose)

diff --git a/tex/generic/pgf/libraries/pgflibraryintersections.code.tex b/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
index 24571576..8be31fe7 100644
--- a/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
+++ b/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
@@ -321,17 +321,23 @@
 }%

 \def\pgf@intersect@line@and@curve{%
+    \ifx\pgfpoint@intersect@start@a\pgfpoint@intersect@end@a
+    \else
     \pgf@intersectionoflineandcurve%
         {\pgf@process{\pgfpoint@intersect@start@a}}{\pgf@process{\pgfpoint@intersect@end@a}}%
         {\pgf@process{\pgfpoint@intersect@start@b}}{\pgf@process{\pgfpoint@intersect@firstsupport@b}}%
         {\pgf@process{\pgfpoint@intersect@secondsupport@b}}{\pgf@process{\pgfpoint@intersect@end@b}}%
+    \fi
 }%

 \def\pgf@intersect@curve@and@line{%
+    \ifx\pgfpoint@intersect@start@b\pgfpoint@intersect@end@b
+    \else
     \pgf@intersectionofcurveandline%
         {\pgf@process{\pgfpoint@intersect@start@a}}{\pgf@process{\pgfpoint@intersect@firstsupport@a}}%
         {\pgf@process{\pgfpoint@intersect@secondsupport@a}}{\pgf@process{\pgfpoint@intersect@end@a}}%
         {\pgf@process{\pgfpoint@intersect@start@b}}{\pgf@process{\pgfpoint@intersect@end@b}}%
+    \fi
 }%

 \def\pgf@intersect@curve@and@curve{%

the time efficiency becomes

BeforeAfter
``` \pgfintersectionofpaths, radius=1cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.222 s (l3benchmark) + TIC (l3benchmark) + TOC: 1.37 s (l3benchmark) + TIC (l3benchmark) + TOC: 2.86 s \pgfintersectionofpaths, radius=3cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.264 s (l3benchmark) + TIC (l3benchmark) + TOC: 4.92 s (l3benchmark) + TIC (l3benchmark) + TOC: 11.3 s ``` ``` \pgfintersectionofpaths, radius=1cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.213 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.142 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.143 s \pgfintersectionofpaths, radius=3cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.259 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.173 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.172 s ```

Notes about the patch above:

The problem is caused by the special close path token (\pgfsyssoftpath@closepathtoken) in the soft path generated by a circle (\pgfpathcircle, also the tikz path operator circle). In general a close path token will be drawn as a line. But the corresponding line for this special token has identical start and ending points, hence is degenerated to a point.

Then when the current algorithm tries to compute the intersection between this special line and a curve, my guess is that some condition is always not met (happens in \pgf@@@intersectionofcurves which I didn't read yet) hence the line and curve are divided into halves and intersections are recomputed using those halves, until the split curve is small enough to pass the tolerance. This process caused exponential time complexity with n proportional to the radius of circle.

Full example, including the patch

```tex \documentclass{article} \usepackage[margin=.5in, landscape]{geometry} \usepackage{tikz} \usetikzlibrary{intersections} \usepackage{l3benchmark} \ExplSyntaxOn \cs_set_eq:NN \BenchmarkTic \benchmark_tic: \cs_set_eq:NN \BenchmarkToc \benchmark_toc: \ExplSyntaxOff % \pgfIntersectionOfPaths{}{} \newcommand{\pgfIntersectionOfPaths}[2]{ \begin{pgfpicture} \BenchmarkTic \pgfintersectionofpaths { #1 \pgfgetpath{\temppath} \pgfusepath{stroke} \pgfsetpath{\temppath} } { #2 \pgfgetpath{\temppath} \pgfusepath{stroke} \pgfsetpath{\temppath} } \BenchmarkToc \foreach \s in {1,...,\pgfintersectionsolutions} {\pgfpathcircle{\pgfpointintersectionsolution{\s}}{2pt}} \pgfusepath{stroke} \end{pgfpicture} } \makeatletter \def\pgf@intersect@line@and@curve{% \ifx\pgfpoint@intersect@start@a\pgfpoint@intersect@end@a \else % duplicate \pgf@process removed \pgf@intersectionoflineandcurve% {\pgfpoint@intersect@start@a}{\pgfpoint@intersect@end@a}% {\pgfpoint@intersect@start@b}{\pgfpoint@intersect@firstsupport@b}% {\pgfpoint@intersect@secondsupport@b}{\pgfpoint@intersect@end@b}% \fi }% \def\pgf@intersect@curve@and@line{% \ifx\pgfpoint@intersect@start@b\pgfpoint@intersect@end@b \else % duplicate \pgf@process removed \pgf@intersectionofcurveandline% {\pgfpoint@intersect@start@a}{\pgfpoint@intersect@firstsupport@a}% {\pgfpoint@intersect@secondsupport@a}{\pgfpoint@intersect@end@a}% {\pgfpoint@intersect@start@b}{\pgfpoint@intersect@end@b}% \fi }% \makeatother \begin{document} \typeout{\string\pgfintersectionofpaths, radius=1cm} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{0}{1cm}}{1cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{60}{1cm}}{1cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{120}{1cm}}{1cm}} \typeout{\string\pgfintersectionofpaths, radius=3cm} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{0}{3cm}}{3cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{60}{3cm}}{3cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{120}{3cm}}{3cm}} \end{document} ```

muzimuzhi commented 1 year ago

The patch shown in my previous comment may cause false negative results for very special input paths, in which the point is detached and is indeed an intersection. Fortunately this is not the case for circles (the point is never detached).

\pgfintersectionofpaths
  {\pgfpathcircle{\pgfpointorigin}{3cm}}
  {\pgfpathmoveto{\pgfpointpolar{60}{3cm}}\pgfpathlineto{\pgfpointpolar{60}{3cm}}}

Quick search suggests that determining if a point is on a quadratic Bezier curve within some tolerance can be done in quadratic time, see for example 1 on math.sx and 2 mentioned by 3. Though TeX's built-in fixed-point representation for dimension might be a barrier.

Dropping the close path token generated by circles is an alternative, only it's too late to recognize circles from a soft path, as I said in https://github.com/pgf-tikz/pgf/issues/1256#issuecomment-1518375863. A special representation of soft path only for intersection calculations?

muzimuzhi commented 1 year ago
  • Looks like it's not necessary (or, it's optional) to add a similar guard for \pgf@intersect@line@and@line.

In general cases, \pgf@intersect@line@and@line needs it too, if the patch in https://github.com/pgf-tikz/pgf/issues/1256#issuecomment-1521373687 is acceptable.

muzimuzhi commented 1 year ago

Dropping the close path token generated by circles is an alternative, only it's too late to recognize circles from a soft path, as I said in #1256 (comment).

The condition "recognizing circles from a soft path" can be loosen. Here's a v2 patch that skips a zero-length close path token.

diff --git a/tex/generic/pgf/libraries/pgflibraryintersections.code.tex b/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
index 24571576..d51772f5 100644
--- a/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
+++ b/tex/generic/pgf/libraries/pgflibraryintersections.code.tex
@@ -219,7 +219,7 @@
                 \let\pgf@intersect@next=\pgf@intersect@token@lineto%
             \else%
                 \ifx#1\pgfsyssoftpath@closepathtoken%
-                    \let\pgf@intersect@next=\pgf@intersect@token@lineto%
+                    \let\pgf@intersect@next=\pgf@intersect@token@closepath
                 \else%
                     \ifx#1\pgfsyssoftpath@curvetosupportatoken%
                         \let\pgf@intersect@next=\pgf@intersect@token@curveto%
@@ -244,6 +244,18 @@
     \def\pgf@intersect@type{line}%
     \pgf@intersect@token@after%
 }%
+
+\def\pgf@intersect@token@closepath#1#2{%
+    \def\pgfpoint@intersect@temp{\pgfqpoint{#1}{#2}}% could directly define @end
+    \ifx\pgfpoint@intersect@start\pgfpoint@intersect@temp
+        \expandafter\pgf@intersectionofpaths
+    \else
+        \let\pgfpoint@intersect@end=\pgfpoint@intersect@temp
+        \def\pgf@intersect@type{line}%
+        \expandafter\pgf@intersect@token@after
+    \fi
+}
+
 \def\pgf@intersect@token@curveto#1#2\pgfsyssoftpath@curvetosupportbtoken#3#4\pgfsyssoftpath@curvetotoken#5#6{%
     \def\pgfpoint@intersect@firstsupport{\pgfqpoint{#1}{#2}}%
     \def\pgfpoint@intersect@secondsupport{\pgfqpoint{#3}{#4}}%
BeforeAfter (v2)After (v1)
``` \pgfintersectionofpaths, radius=1cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.222 s (l3benchmark) + TIC (l3benchmark) + TOC: 1.37 s (l3benchmark) + TIC (l3benchmark) + TOC: 2.86 s \pgfintersectionofpaths, radius=3cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.264 s (l3benchmark) + TIC (l3benchmark) + TOC: 4.92 s (l3benchmark) + TIC (l3benchmark) + TOC: 11.3 s [SLOW] detached point is the intersection (l3benchmark) + TIC (l3benchmark) + TOC: 11.1 s ``` ``` \pgfintersectionofpaths, radius=1cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.212 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.142 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.143 s \pgfintersectionofpaths, radius=3cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.267 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.176 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.173 s [SLOW] detached point is the intersection (l3benchmark) + TIC (l3benchmark) + TOC: 11.1 s ``` ``` \pgfintersectionofpaths, radius=1cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.213 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.142 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.143 s \pgfintersectionofpaths, radius=3cm (l3benchmark) + TIC (l3benchmark) + TOC: 0.259 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.173 s (l3benchmark) + TIC (l3benchmark) + TOC: 0.172 s [SLOW] detached point is the intersection (l3benchmark) + TIC (l3benchmark) + TOC: 8.09e-4 s ```

Notes about v2 patch

Full example showing v2 patch

```tex \documentclass{article} \usepackage[margin=.5in, landscape]{geometry} \usepackage{tikz} \usetikzlibrary{intersections} \usepackage{l3benchmark} \usepackage{xpatch} \ExplSyntaxOn \cs_set_eq:NN \BenchmarkTic \benchmark_tic: \cs_set_eq:NN \BenchmarkToc \benchmark_toc: \ExplSyntaxOff % \pgfIntersectionOfPaths{}{} \newcommand{\pgfIntersectionOfPaths}[2]{ \begin{pgfpicture} \BenchmarkTic \pgfintersectionofpaths { #1 \pgfgetpath{\temppath} \pgfusepath{stroke} \pgfsetpath{\temppath} } { #2 \pgfgetpath{\temppath} \pgfusepath{stroke} \pgfsetpath{\temppath} } \BenchmarkToc \ifnum\pgfintersectionsolutions>0\relax \foreach \s in {1,...,\pgfintersectionsolutions} {\pgfpathcircle{\pgfpointintersectionsolution{\s}}{2pt}} \pgfusepath{stroke} \fi \end{pgfpicture} } \makeatletter % add a handler for close path token \xpatchcmd\pgf@intersectionofpaths {\ifx#1\pgfsyssoftpath@closepathtoken \let\pgf@intersect@next=\pgf@intersect@token@lineto} {\ifx#1\pgfsyssoftpath@closepathtoken \let\pgf@intersect@next=\pgf@intersect@token@closepath} {}{\PatchFailed} \def\pgf@intersect@token@closepath#1#2{% \def\pgfpoint@intersect@temp{\pgfqpoint{#1}{#2}}% could directly define @end \ifx\pgfpoint@intersect@start\pgfpoint@intersect@temp \expandafter\pgf@intersectionofpaths \else \let\pgfpoint@intersect@end=\pgfpoint@intersect@temp \def\pgf@intersect@type{line}% \expandafter\pgf@intersect@token@after \fi } \makeatother \begin{document} \typeout{\string\pgfintersectionofpaths, radius=1cm} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{0}{1cm}}{1cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{60}{1cm}}{1cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{1cm}} {\pgfpathcircle{\pgfpointpolar{120}{1cm}}{1cm}} \typeout{\string\pgfintersectionofpaths, radius=3cm} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{0}{3cm}}{3cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{60}{3cm}}{3cm}} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathcircle{\pgfpointpolar{120}{3cm}}{3cm}} \typeout{[SLOW] detached point is the intersection} \pgfIntersectionOfPaths {\pgfpathcircle{\pgfpointorigin}{3cm}} {\pgfpathmoveto{\pgfpointpolar{60}{3cm}}\pgfpathlineto{\pgfpointpolar{60}{3cm}}} \end{document} ```

muzimuzhi commented 1 year ago
  • Zero-length line-to tokens are a bit harder to handler. It is skippable only if it has been or will be on other line-to or curve-to tokens. In simple cases one can only check if it follows a line-to or curve-to (hence it shares end points with previous drawing token).

Zero-length close path tokens need similar treatment if the corresponding subpath is empty (a close path token right after a move-to token).

From another side, one can argue if zero-length close path or line-to tokens should be counted when calculating intersections. After all such tokens are never drawn so here we are really computing invisible intersections.