10

I have a path consisting of a series of straight lines and arcs, and now I would like to modify that path by replacing some segments of the line/the arc with another small arc whose centre is on the original paths (see picture below).

The kind of path transformation I want to have (explained in words above)

Ideally I would like to only specify the original path and the locations (in per cent) of the original path where the detours should be added (and whether they should go to the left or to the right). The radius of all the small detour arcs is the same.

Is something like that possible (e.g. with decorations)?

I know how to do it manually, but that gets fairly tedious once you have a few of those detours, and you have to readjust a lot of things if you change the size of things.

UPDATE

As an example of what I had in mind, here's a curve with corresponding TikZ code as generated by a simple Haskell program. In the program I simply have to lists that specify at which y positions the vertical lines get wiggles and at which angles the arc gets wiggles. The rest involves only a little fiddling with the arc sine function.

This was more what I had in mind: To create a path using some macros (essentially just a "for each" loop).

A curve whose TikZ code has been generated by a program

\documentclass{article}
\usepackage{tikz}
\begin{document}
\begin{tikzpicture}[scale=6]
\draw[color=red,line width=0.4mm] (0.5,0.8660)arc (60.0000:72.1349:1) arc (-16.4325:166.4325:0.0500) arc (77.8651:87.1349:1) arc (-1.4325:181.4325:0.0500) arc (92.8651:102.1349:1) arc (373.5675:196.4325:0.0500) arc (107.8651:120.0000:1) -- (-0.5000, 1.0660) arc (270:90:0.0500) -- (-0.5000, 1.3160) arc (270:90:0.0500) -- (-0.5000, 1.5660) arc (270:90:0.0500) -- (-0.5000, 1.8660)  -- (0.5,1.8660) -- (0.5000, 1.6660) arc (90:270:0.0500) -- (0.5000, 1.4160) arc (90:270:0.0500) -- (0.5000, 1.1660) arc (90:270:0.0500) -- cycle;
\end{tikzpicture}
\end{document}

And the program that generated it:

import Text.Printf

foo' :: Double -> Double -> Double -> [Double] -> Double -> String
foo' r x l [] u = printf "-- (%.4f, %.4f) " x u
foo' r x l (y : ys) u = printf "-- (%.4f, %.4f) arc (270:90:%.4f) " x (y - r) r ++ foo' r x (y + r) ys u

foo'' :: Double -> Double -> Double -> [Double] -> Double -> String
foo'' r x l [] u = printf "-- cycle;"
foo'' r x l (y : ys) u = printf "-- (%.4f, %.4f) arc (90:270:%.4f) " x (y + r) r ++ foo'' r x (y - r) ys u

bar' :: Double -> Double -> [(Bool, Double)] -> Double -> String
bar' r l [] u = printf "arc (%.4f:%.4f:1) " l u
bar' r l ((left, y) : ys) u = printf "arc (%.4f:%.4f:1) arc (%.4f:%.4f:%.4f) " l (y - alpha) (y - s * beta + delta) (y + s * beta) r ++ bar' r (y + alpha) ys u
  where alpha = 2 * asin (r/2) * 180 / pi
        beta = asin (r / 2) * 180 / pi + 90
        s = signum (u - l)
        delta = if left /= (l > u) then 0 else 360 * s

ys = map ((+) (sqrt 3 / 2)) [0.25, 0.5, 0.75]
phis = [(True, 75), (True, 90), (False, 105)]

narf r h ys phis = "\\draw[contour1] " ++
   printf "(0.5,%.4f)" (sqrt 3 / 2 :: Double) ++
   bar' r 60 phis 120 ++
   foo' r (-0.5) (sqrt 3 / 2) ys (h + sqrt 3 / 2) ++
   printf " -- (0.5,%.4f) " (h + sqrt 3 / 2) ++
   foo'' r 0.5 (h + sqrt 3 / 2) (reverse ys) (sqrt 3 / 2)

main = 
  do putStrLn $ narf 0.05 1 ys phis
4
  • The tikz library spath3 can do this easily. How are you specifying the points where you want the arcs spliced in? With a curved path, are you concerned about pinpoint accuracy with the size of the spliced arcs, or is a little variation acceptable? Apr 5, 2022 at 12:43
  • I thought spath3 might be what I was looking for. What I'd ideally like to do is something like \draw (0,0) -- (5, 0) and then some modifier that says something like ‘insert a wiggle of radius 0.2 after 30% of the path’, and similarly if I have \draw (0,0) arc (60:120:1). Apr 5, 2022 at 15:30
  • And it would be good if this also worked for composite paths, i.e. paths that consist of alternating lines and arcs. But even if it just works for a single line/arc that would already be great! Apr 5, 2022 at 15:31
  • 1
    The inserted arcs should all have the same radius though. I could easily write a program in e.g. Python that does the maths for computing where and how to draw the arcs, but doing that sort of thing in TeX/TikZ is always very painful to me. Apr 5, 2022 at 15:32

3 Answers 3

9

Here's two ways that spath3 can be used for this.

Method 1:

Given a parameter specifying a point on the path, it splits the path at that point. Then it inserts a gap of a particular width at each cut point, before finally splicing in a semi-circular arc to refill the gaps.

There are, though, a few "here be dragons":

  1. Some of the pieces of this (most notably the split at key) are only in the development version so at time of writing you'll need to download that from github (linked above). I'm pretty confident that it works, but the user interface might not be the best (so ... any comments will be gratefully received!).
  2. The way the path parameter works is outlined in Section 2.9 of the documentation for the library (in the development version), but in short the entire path is mapped to the interval [0,1] in a way that makes most sense for the code. It is hopefully not confusing once you've read it, but it does mean that when a path is split then the parametrisation changes. This explains the weird point specifications, such as 0.6/0.8*1/2. I'm not ecstatic about it, but my instinct is that trying to be "clever" will lead to more difficulties than leaving it as it is.
  3. The gaps are approximations, not exact. The way that the gap size is calculated uses the first derivative of the path at the point to convert a dimension to a parameter. This is not perfect (except for lines). I went for this option, together with the semi-circular arcs, for simplicity of coding (as I already had a fair amount of the code already existing).

Method 2:

This strategy creates circles at each location and then splits the original path and the set of circles at the points where they intersect. Then by welding together a judicious choice of segments, the desired path can be created. This method is probably closer to the original intent, but is computationally more complicated as it involves working out a lot of intersections (it can be up to four per arc, as sometimes the code needs to work out each intersection twice - once from the perspective of each path). On the plus side, the arcs are guaranteed to be centred at the specified point on the path and are guaranteed to have the correct radius. It's also a bit more fiddly to get the "magic numbers" right (compare the choices of components in the circles path for the line and the curve).

Code

Here's the code for both methods.

\documentclass{article}
%\url{https://tex.stackexchange.com/q/639719/86}
\usepackage{tikz}
\usetikzlibrary{
  intersections, % Needed for method 2
  spath3
}

\begin{document}

% Method 1: Splice into gaps

\begin{tikzpicture}[
  original path/.style={
    ultra thick,
    red,
    spath/save=#1
  }
]
\path[spath/save=arc,overlay] (0,0) arc[radius=1cm, start angle=180, delta angle=-180];
\draw[original path=line] (0,0) -- (3,0);
\tikzset{
  spath/split at={line}{0.8},
  spath/split at={line}{0.6/.8*1/2},
  spath/split at={line}{0.4/.6*1/3},
  spath/split at={line}{0.2/.4*1/4},
  spath/insert gaps after components={line}{10pt},
  spath/join components with={line}{arc}
}
\draw[spath/use=line];
\draw[original path=curve] (0,-1) .. controls +(1,-1) and +(-1,-1) .. +(3,0);
\tikzset{
  spath/split at={curve}{0.8},
  spath/split at={curve}{0.6/.8*1/2},
  spath/split at={curve}{0.4/.6*1/3},
  spath/split at={curve}{0.2/.4*1/4},
  spath/insert gaps after components={curve}{10pt},
  spath/join components with={curve}{arc}
}
\draw[spath/use=curve];

\end{tikzpicture}

% Method 2: split with circles

\begin{tikzpicture}[
  original path/.style={
    ultra thick,
    red,
    spath/save=#1
  }
]
\draw[original path=line] (0,0) -- (3,0);
\draw[original path=circles]
(spath cs:line 0.2) circle[radius=5pt]
(spath cs:line 0.4) circle[radius=5pt]
(spath cs:line 0.6) circle[radius=5pt]
(spath cs:line 0.8) circle[radius=5pt]
;


\tikzset{
  spath/remove empty components={circles},
  spath/split at intersections={line}{circles},
  spath/get components of={line}\lineCpts, 
  spath/get components of={circles}\circleCpts,
}
\draw[
  spath/use=\getComponentOf\lineCpts{1},
  spath/use={\getComponentOf\circleCpts{1},weld,reverse},
  spath/use=\getComponentOf\lineCpts{3},
  spath/use={\getComponentOf\circleCpts{3},weld,reverse},
  spath/use=\getComponentOf\lineCpts{5},
  spath/use={\getComponentOf\circleCpts{5},weld,reverse},
  spath/use=\getComponentOf\lineCpts{7},
  spath/use={\getComponentOf\circleCpts{7},weld,reverse},
  spath/use=\getComponentOf\lineCpts{9},
];

\draw[original path=curve] (0,-1) .. controls +(1,-1) and +(-1,-1) .. +(3,0);

\draw[original path=circles]
(spath cs:curve 0.2) circle[radius=5pt]
(spath cs:curve 0.4) circle[radius=5pt]
(spath cs:curve 0.6) circle[radius=5pt]
(spath cs:curve 0.8) circle[radius=5pt]
;


\tikzset{
  spath/remove empty components={circles},
  spath/split at intersections={curve}{circles},
  spath/get components of={curve}\curveCpts, 
  spath/get components of={circles}\circleCpts,
}
\draw[
  spath/use=\getComponentOf\curveCpts{1},
  spath/use={\getComponentOf\circleCpts{2},weld,reverse},
  spath/use=\getComponentOf\curveCpts{3},
  spath/use={\getComponentOf\circleCpts{4},weld,reverse},
  spath/use=\getComponentOf\curveCpts{5},
  spath/use={\getComponentOf\circleCpts{5},weld,reverse},
  spath/use=\getComponentOf\curveCpts{7},
  spath/use={\getComponentOf\circleCpts{7},weld,reverse},
  spath/use=\getComponentOf\curveCpts{9},
];

\end{tikzpicture}


\end{document}

Result

Paths with arcs inserted at positions

If you have any suggestions on how to improve the spath3 interface, please do open an issue on github (or drop me an email if you're not on github).

4
  • Well this certainly looks pretty much exactly like what I had in mind! I can't say I understand how any of it works though. How would I change the direction of some of these semicircles? Apr 5, 2022 at 19:38
  • In any case, I was hoping for a solution involving less boilerplate. In principle, the idea seems pretty easy since my original paths are only lines and arcs, whose geometry is fairly simple. But still, thanks for this solution of course! Apr 5, 2022 at 19:39
  • I don't understand what you mean by "less boilerplate", could you elaborate? Changing the direction of the arcs is simple. With method 1, you can define a second splicing path and then designate different components to join between - look at the bridging examples in the documentation. With method 2, just choose different components of the circles path (and remove the reverse key). Apr 5, 2022 at 21:32
  • 1
    I really need to learn spath3. +1
    – Sandy G
    Apr 6, 2022 at 12:56
5

I would use Asymptote, a programming language, for plenty of path operations. TikZ is okay (see @Andrew Stacy's above answer) with more complicated calculations and coding.

This way is not in full generality, but good enough. The idea is simple: calculating intersection points of circles with the curve, and relevant join them. You can easily customize via radius r, array t of times (positions) of centers of circular arcs, like this

real[] t={.08,.15,.22,.35,.4,.45,.82,.88,.95};

and array tdirection of directions of circular arcs, like this

bool[] tdirection={CCW,CW,CW,CW,CW,CW,CCW,CCW,CCW};

enter image description here

Full code

// http://asymptote.ualberta.ca/
unitsize(1cm);
real r=.18;           // radius of the circular arcs
path p=(0,0) .. controls 2dir(45) and (5,0)+dir(120) .. (5,0)
  --(5,4)--(0,4)--cycle;
//draw(p,lightgray+3pt);

real[] t={.08,.15,.22,.35,.4,.45,.82,.88,.95};  // times of centers of a circular arc
bool[] tdirection={CCW,CW,CW,CW,CW,CW,CCW,CCW,CCW}; // to control the direction of arcs
pen pcolor=rgb(239,114,21)+.8pt;     // carrot ^^
real[] te;                 // times of removed arcs    
te.push(0);
for(int i=0;i<t.length;++i){
pair A=relpoint(p,t[i]);     // center of a circular arc
real[][] s=intersections(p,circle(A,r)); 
te.push(s[0][0]);
te.push(s[1][0]);
pair[] B=intersectionpoints(p,circle(A,r));
path parc=arc(A,B[0],B[1],tdirection[i]);
draw(parc,pcolor);
}
te.push(length(p));

for(int i=0;i<te.length;i=i+2){
path q=subpath(p,te[i],te[i+1]);
draw(q,pcolor);
}

shipout(bbox(5mm,invisible));

Explanation: For simplicity, first consider the case of 1 circular arc.

enter image description here

// http://asymptote.ualberta.ca/
unitsize(1cm);
real r=.15;           // radius of the circular arcs
path p=(0,-1) .. controls (0,-1)+(6,-1) and (3,-1)+(-6,-1) .. (3,-1);
draw(p,lightgray+3pt);

real t=.38;               // change as you wish, time t is in (0,1)
pair A=relpoint(p,t);     // center of a circular arc
real[][] s=intersections(p,circle(A,r)); 
pair[] B=intersectionpoints(p,circle(A,r));

path q1=subpath(p,0,s[0][0]);
path q2=arc(A,B[0],B[1],CW);    // default is CCW - counter-clockwise, to change orientation of the circular arc
path q3=subpath(p,s[1][0],length(p));

draw(q1^^q3,magenta);
draw(q2,blue);

shipout(bbox(5mm,invisible));

For t=.55

enter image description here

Now for the case of several circular arcs, I just create a new array te to store times at intersections. An unexpected behavior can be happend with some odd shapes of the initial curves and some value of radius r, but I am not trying to handle with the most general case, because I think it is not necessary. Another array, tdirection is for choosing orientation of arcs.

enter image description here

unitsize(1cm);
real r=.15;           // radius of the circular arcs
path p=(0,-1) .. controls (0,-1)+(6,-1) and (3,-1)+(-6,-1) .. (3,-1);
draw(p,lightgray+3pt);

real[] t={.1,.38,.55,.85,.95};  // times of centers of a circular arc
real[] te;                 // times of removed arcs    
bool[] tdirection={CCW,CW,CW,CCW,CW}; // to control the direction of arcs
te.push(0);
for(int i=0;i<t.length;++i){
pair A=relpoint(p,t[i]);     // center of a circular arc
real[][] s=intersections(p,circle(A,r)); 
te.push(s[0][0]);
te.push(s[1][0]);
pair[] B=intersectionpoints(p,circle(A,r));
path q2=arc(A,B[0],B[1],tdirection[i]);
draw(q2,blue);
}
te.push(1);

for(int i=0;i<te.length;i=i+2){
path q=subpath(p,te[i],te[i+1]);
draw(q,magenta);
}

shipout(bbox(5mm,invisible));
3

Here is a simple solution using pics that works perfectly for straight lines, but needs some manual adjusting for curves.

enter image description here

We define a pic that consists of a white line (thicker than the current line thickness to "erase" a portion of the line), and a semicircle (lengthened by half the line thickness to make a clean line join).

The basic call is \draw(0,0) to pic[pos=.4]{scirc=-}pic[pos=.7]{scirc} (3,0);.

The pos= value indicates the position of the center of the semicircle, so pos=0 or pos=1 will go beyond the segment.

enter image description here

scirc draws the semicircle "above" the line. To switch sides, use scirc=-.

If the line is not horizontal, you must add sloped to the pic options:

\draw(0,0) to pic[pos=.4,sloped]{scirc=-}pic[pos=.7,sloped]{scirc} (3,1);

enter image description here

The radius of the semicircles is set globally with \srad.

Here is the code:

\documentclass{article}

\usepackage{tikz}

\tikzset{scirc/.pic={\draw[white, line width=1.5\pgflinewidth] (-\srad,0) -- (\srad,0); 
    \draw[shorten >=-.5\pgflinewidth,shorten <=-.5\pgflinewidth] (-\srad,0) arc (#1 180:0:\srad);},
    scirc/.default={}}

\newcommand{\srad}{2mm}

\begin{document}

\begin{tikzpicture}
\draw[thick,blue](0,0) to pic[pos=.4]{scirc=-}pic[pos=.7]{scirc} (3,0) 
    to pic[pos=.8,sloped]{scirc} (5,2)
    to pic[pos=.8,sloped]{scirc} (3,3)
    to pic[pos=.2,sloped]{scirc}pic[pos=.5,sloped]{scirc=-} cycle;
\end{tikzpicture}

\end{document}

If you try this on a curve, you get bad line joins:

enter image description here

To fix this you need to manually xshift and/or yshift the pic. In this example, pic[pos=.25,sloped,yshift=-.04pt,transform shape]{scirc} is pretty good:

enter image description here

\newcommand{\srad}{.5mm} % small semicircles here

\begin{tikzpicture} 
\draw[color=red] (0.5,0.866) to[bend right]pic[pos=.25,sloped,yshift=-.04pt,transform shape]{scirc}pic[pos=.5,sloped,yshift=-.04pt,transform shape]{scirc}pic[pos=.75,sloped,yshift=-.04pt,transform shape]{scirc=-} (-0.5,0.866) 
    to pic[pos=.25,sloped]{scirc}pic[pos=.5,sloped]{scirc}pic[pos=.75,sloped]{scirc} (-0.5, 1.866) 
    to (0.5,1.866) to pic[pos=.25,sloped]{scirc=-}pic[pos=.5,sloped]{scirc=-}pic[pos=.75,sloped]{scirc=-} cycle;
\end{tikzpicture}

Note: To use the arc command, you must use the "long form":

\draw[teal](0,0)  arc[start angle=120,end angle=60,radius=1] 
    pic[pos=.8,sloped,yshift=-.04pt,transform shape]{scirc};

enter image description here

5
  • Yes, your solution works well for straight segments! "If you try this on a curve, you get bad line joins" >>> the problem is not only line joins, but the arc is not semi-circular arc for curvy curves.
    – Black Mild
    Apr 6, 2022 at 9:06
  • @BlackMild: It works the same for circular or elliptical arcs, but you have to use the "long form" of the arc command: \draw(0,0) arc[start angle=120,end angle=60,radius=2]pic[pos=.8,sloped]{scirc}
    – Sandy G
    Apr 6, 2022 at 11:51
  • 1
    @BlackMild: I added an example in my solution.
    – Sandy G
    Apr 6, 2022 at 12:01
  • how about the circular arc at the point of a sharp corners (see e.g. the 1st figure in my answer) ? or pos=.98 in your last code pic[pos=.98,sloped,yshift=-.04pt,transform shape,blue]{scirc=-} (-0.5,0.866)
    – Black Mild
    Apr 6, 2022 at 12:10
  • 1
    @BlackMild: pos=.98 forces the semicircle beyond the segment since the pos= value indicates the center of the semicircle. For very sharp corners I am unclear what OP wants. My interpretation was that the "detours" were always supposed to be semicircles, but maybe OP had something else in mind.
    – Sandy G
    Apr 6, 2022 at 12:24

You must log in to answer this question.

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