83

This question led to a new package:
hobby

I found Metapost best for drawing complex smooth curves (i.e., Bezier, splines), since you do not have to directly specify Bezier control points. Unfortunately, I need to use TikZ for my current project exclusively; drawing (closed) curves in TikZ is tedious and very time-consuming task. So I combined the "power" of Metapost with TikZ into the following workflow:

  • Draw closed curve in Metapost.
  • Open the generated Postscript file in a text editor and manually extract control points.
  • Paste extracted points into a TikZ figure and modify the PGF/TikZ expressions to draw curve.

Pasted below is a reproducible example to illustrate the described approach.

%% Construct curve in Metapost
beginfig(1)
  draw (0,0) .. (60,40) .. (40,90) .. (10,70) .. (30,50) .. cycle;
endfig;
end

%% Extract control points from postscript file
newpath 0 0 moveto
5.18756 -26.8353 60.36073 -18.40036 60 40 curveto
59.87714 59.889 57.33896 81.64203 40 90 curveto
22.39987 98.48387 4.72404 84.46368 10 70 curveto
13.38637 60.7165 26.35591 59.1351 30 50 curveto
39.19409 26.95198 -4.10555 21.23804 0 0 curveto closepath stroke

%% Create Tikz figure in pdfLaTeX
\documentclass{standalone}
\usepackage{tikz}
\begin{document}
\begin{tikzpicture}[scale=0.1] 
\draw (0, 0) .. controls (5.18756, -26.8353) and (60.36073, -18.40036)
   .. (60, 40) .. controls (59.87714, 59.889) and (57.33896, 81.64203)
   .. (40, 90) .. controls (22.39987, 98.48387) and (4.72404, 84.46368)
   .. (10, 70) .. controls (13.38637, 60.7165) and (26.35591, 59.1351)
   .. (30, 50) .. controls (39.19409, 26.95198) and (-4.10555, 21.23804)
   .. (0, 0);    
\end{tikzpicture}
\end{document}

This approach works if you have to draw a curve or two, but becomes tedious with more curves. I wonder if there is a simpler way which avoids manual copy-paste repetitions from file to file? Maybe the most elegant solution should be a simple C/C++/... program, but I cant't find the implementation of Hobby's algorithm used by Metapost to calculate Bezier control points. Any ideas would be greatly appreciated.


Addition by Jake:

Here's a comparison of the path resulting from Hobby's algorithm (thick black line) and the \draw plot [smooth] algorithm (orange line). In my opinion, the result from Hobby's algorithm is clearly superior in this case.

Charles Stewart
  • 21,014
  • 5
  • 65
  • 121
Andrej
  • 2,742
  • 1
    Here's a related question: http://tex.stackexchange.com/q/33607/86 though it may be that none of the answers there is quite "polished" enough. I'd quite like to see Hobby's algorithm implemented in TikZ. – Andrew Stacey May 07 '12 at 12:57
  • @Jake: Beat me to it! I was thinking this a worthy recipient of a bounty as well. – Andrew Stacey May 08 '12 at 12:22
  • @AndrewStacey: And with a bit of work, you might become the worthy recipient of the bounty! Go get it =) – Jake May 08 '12 at 12:24
  • Probably not what you want, but couldn't you just produce a pdf with metapost and then include that pdf into your document? – matth May 10 '12 at 13:58
  • @Andrej: I'll need to award the bounty to one of the answers in three days, but I find it quite hard to decide which one is "best" - they're all amazing. Would you mind commenting which solution works best for your application? – Jake May 12 '12 at 18:07
  • @Jake: Yes, It's as you say - all solutions areI give you the last word. – Andrej May 13 '12 at 09:00
  • So, is the moral of the story that TikZ should be using Hobby's algorithm instead of whatever it is using? – Faheem Mitha May 19 '12 at 22:51
  • @FaheemMitha Not really! TikZ's current algorithm for smooth paths is much faster than Hobby's algorithm. – Andrew Stacey May 21 '12 at 08:31
  • (At least, than Hobby's algorithm in TeX. Exporting the problem to lua or python or whatever is obviously quite a bit faster.) – Andrew Stacey May 21 '12 at 08:31
  • @AndrewStacey: I thought the point was that Hobby was a better algorithm, not faster. – Faheem Mitha May 21 '12 at 09:32
  • @FaheemMitha Exactly. Hobby's algorithm is better but slower. So TikZ's current implementation is a "quick fix" to getting a smooth path, but the results aren't always as good as they could be. – Andrew Stacey May 21 '12 at 10:07

6 Answers6

65

This question led to a new package:
hobby

Update (17th May 2012): Preliminary code now on TeX-SX Launchpad: download hobby.dtx and run pdflatex hobby.dtx. Now works with closed curves, and with tensions and other options.


I am, frankly, astonished that I got this to work. It is somewhat limited - it works for open paths only and doesn't allow all the flexibility of the original algorithm in that I assume that the "tensions" and "curls" are set to 1. Compared to the work it took to get this far, doing the rest shouldn't be a major hassle! However, I'm quite exhausted at the amount I've done so I'll post this and see if anyone likes it.

I'll also say at this point that if it hadn't been for JLDiaz's python solution I probably would still be debugging it five years from now. The python script is so well done and well commented that even someone who has never (well, hardly ever) written a python script could add the necessary "print" statements to see all the results of the various computations that go on. That meant I had something to compare my computations against (so anyone who votes for this answer should feel obliged to vote for JLDiaz's as well!).

It is a pure LaTeX solution. In fact, it is LaTeX3 - and much fun it was learning to program using LaTeX3! This was my first real experience in programming LaTeX3 so there's probably a lot that could be optimised. I had to use one routine from pgfmath: the atan2 function. Once that is in LaTeX3, I can eliminate that stage as well.

Here's the code: (Note: 2012-08-31 I've removed the code from this answer as it is out of date. The latest code is now available on TeX-SX Launchpad.)

And here's the result, with the MetaPost version underneath, and the control points of the curves shown via the show curve controls style from the PGF manual.

Hobby's algorithm in LaTeX3


Update (2012-08-31)

I had reason to revisit this because I wanted a version of Hobby's algorithm where adding points on to the end of the path did not change the earlier part (at least, there was some point beyond which the path did not change). In Hobby's algorithm, the effect of a point dissipates exponentially but changing one point still changes the entire path. So what I ended up doing was running Hobby's algorithm on subpaths. I consider each triple of points and run the algorithm with just those three points. That gives me two bezier curves. I keep the first and throw the second away (unless I'm at the end of the list). But, I remember the angle at which the two curves joined and ensure that when I consider the next triple of points then that angle is used (Hobby's algorithm allows you to specify the incoming angle if you so wish).

Doing it this way means that I avoid solving large linear systems (even if they are tridiagonal): I have to solve one 2x2 for the first subpath and after that there's a simple formula for the rest. This also means that I no longer need arrays and the like.

In the implementation, I've ditched all the tension and curl stuff - this is meant to be the quick method after all. It would be possible to put that back. It also means that it becomes feasible (for me) in PGFMath so this is 100% LaTeX3-free. It also doesn't make sense for closed curves (since you need to choose a place to start). So in terms of features, it's pretty poor when compared to the above full implementation. But it's a bit smaller and quicker and gets pretty good results.

Here's the crucial code:

\makeatletter
\tikzset{
  quick curve through/.style={%
    to path={%
      \pgfextra{%
      \tikz@scan@one@point\pgfutil@firstofone(\tikztostart)%
        \edef\hobby@qpointa{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}%
        \def\hobby@qpoints{}%
        \def\hobby@quick@path{}%
        \def\hobby@angle{}%
        \def\arg{#1}%
        \tikz@scan@one@point\hobby@quick#1 (\tikztotarget)\relax
      }
      \hobby@quick@path
    }
  }
}

\pgfmathsetmacro\hobby@sf{10cm}

\def\hobby@quick#1{%
  \ifx\hobby@qpoints\pgfutil@empty
  \else
  #1%
  \pgf@xb=\pgf@x
  \pgf@yb=\pgf@y
  \hobby@qpointa
  \pgf@xa=\pgf@x
  \pgf@ya=\pgf@y
  \advance\pgf@xb by -\pgf@xa
  \advance\pgf@yb by -\pgf@ya
  \pgfmathsetmacro\hobby@done{sqrt((\pgf@xb/\hobby@sf)^2 + (\pgf@yb/\hobby@sf)^2)}%
  \pgfmathsetmacro\hobby@omegaone{rad(atan2(\pgf@xb,\pgf@yb))}%
  \hobby@qpoints
  \advance\pgf@xa by -\pgf@x
  \advance\pgf@ya by -\pgf@y
  \pgfmathsetmacro\hobby@dzero{sqrt((\pgf@xa/\hobby@sf)^2 + (\pgf@ya/\hobby@sf)^2)}%
  \pgfmathsetmacro\hobby@omegazero{rad(atan2(\pgf@xa,\pgf@ya))}%
  \pgfmathsetmacro\hobby@psi{\hobby@omegaone - \hobby@omegazero}%
  \pgfmathsetmacro\hobby@psi{\hobby@psi > pi ? \hobby@psi - 2*pi : \hobby@psi}%
  \pgfmathsetmacro\hobby@psi{\hobby@psi < -pi ? \hobby@psi + 2*pi : \hobby@psi}%
  \ifx\hobby@angle\pgfutil@empty
  \pgfmathsetmacro\hobby@thetaone{-\hobby@psi * \hobby@done /(\hobby@done + \hobby@dzero)}%
  \pgfmathsetmacro\hobby@thetazero{-\hobby@psi - \hobby@thetaone}%
  \let\hobby@phione=\hobby@thetazero
  \let\hobby@phitwo=\hobby@thetaone
  \else
  \let\hobby@thetazero=\hobby@angle
  \pgfmathsetmacro\hobby@thetaone{-(2 * \hobby@psi + \hobby@thetazero) * \hobby@done / (2 * \hobby@done + \hobby@dzero)}%
  \pgfmathsetmacro\hobby@phione{-\hobby@psi - \hobby@thetaone}%
  \let\hobby@phitwo=\hobby@thetaone
  \fi
  \let\hobby@angle=\hobby@thetaone
  \pgfmathsetmacro\hobby@alpha{%
    sqrt(2) * (sin(\hobby@thetazero r) - 1/16 * sin(\hobby@phione r)) * (sin(\hobby@phione r) - 1/16 * sin(\hobby@thetazero r)) * (cos(\hobby@thetazero r) - cos(\hobby@phione r))}%
  \pgfmathsetmacro\hobby@rho{%
    (2 + \hobby@alpha)/(1 + (1 - (3 - sqrt(5))/2) * cos(\hobby@thetazero r) +  (3 - sqrt(5))/2 * cos(\hobby@phione r))}%
  \pgfmathsetmacro\hobby@sigma{%
    (2 - \hobby@alpha)/(1 + (1 - (3 - sqrt(5))/2) * cos(\hobby@phione r) +  (3 - sqrt(5))/2 * cos(\hobby@thetazero r))}%
  \hobby@qpoints
  \pgf@xa=\pgf@x
  \pgf@ya=\pgf@y
  \pgfmathsetlength\pgf@xa{%
    \pgf@xa + \hobby@dzero * \hobby@rho * cos((\hobby@thetazero + \hobby@omegazero) r)/3*\hobby@sf}%
  \pgfmathsetlength\pgf@ya{%
    \pgf@ya + \hobby@dzero * \hobby@rho * sin((\hobby@thetazero + \hobby@omegazero) r)/3*\hobby@sf}%
  \hobby@qpointa
  \pgf@xb=\pgf@x
  \pgf@yb=\pgf@y
  \pgfmathsetlength\pgf@xb{%
    \pgf@xb - \hobby@dzero * \hobby@sigma * cos((-\hobby@phione + \hobby@omegazero) r)/3*\hobby@sf}%
  \pgfmathsetlength\pgf@yb{%
    \pgf@yb - \hobby@dzero * \hobby@sigma * sin((-\hobby@phione + \hobby@omegazero) r)/3*\hobby@sf}%
  \hobby@qpointa
  \edef\hobby@quick@path{\hobby@quick@path .. controls (\the\pgf@xa,\the\pgf@ya) and (\the\pgf@xb,\the\pgf@yb) .. (\the\pgf@x,\the\pgf@y) }%
  \fi
  \let\hobby@qpoints=\hobby@qpointa
  #1
  \edef\hobby@qpointa{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}%
  \pgfutil@ifnextchar\relax{%
  \pgfmathsetmacro\hobby@alpha{%
    sqrt(2) * (sin(\hobby@thetaone r) - 1/16 * sin(\hobby@phitwo r)) * (sin(\hobby@phitwo r) - 1/16 * sin(\hobby@thetaone r)) * (cos(\hobby@thetaone r) - cos(\hobby@phitwo r))}%
  \pgfmathsetmacro\hobby@rho{%
    (2 + \hobby@alpha)/(1 + (1 - (3 - sqrt(5))/2) * cos(\hobby@thetaone r) +  (3 - sqrt(5))/2 * cos(\hobby@phitwo r))}%
  \pgfmathsetmacro\hobby@sigma{%
    (2 - \hobby@alpha)/(1 + (1 - (3 - sqrt(5))/2) * cos(\hobby@phitwo r) +  (3 - sqrt(5))/2 * cos(\hobby@thetaone r))}%
  \hobby@qpoints
  \pgf@xa=\pgf@x
  \pgf@ya=\pgf@y
  \pgfmathsetlength\pgf@xa{%
    \pgf@xa + \hobby@done * \hobby@rho * cos((\hobby@thetaone + \hobby@omegaone) r)/3*\hobby@sf}%
  \pgfmathsetlength\pgf@ya{%
    \pgf@ya + \hobby@done * \hobby@rho * sin((\hobby@thetaone + \hobby@omegaone) r)/3*\hobby@sf}%
  \hobby@qpointa
  \pgf@xb=\pgf@x
  \pgf@yb=\pgf@y
  \pgfmathsetlength\pgf@xb{%
    \pgf@xb - \hobby@done * \hobby@sigma * cos((-\hobby@phitwo + \hobby@omegaone) r)/3*\hobby@sf}%
  \pgfmathsetlength\pgf@yb{%
    \pgf@yb - \hobby@done * \hobby@sigma * sin((-\hobby@phitwo + \hobby@omegaone) r)/3*\hobby@sf}%
  \hobby@qpointa
  \edef\hobby@quick@path{\hobby@quick@path .. controls (\the\pgf@xa,\the\pgf@ya) and (\the\pgf@xb,\the\pgf@yb) .. (\the\pgf@x,\the\pgf@y) }%
}{\tikz@scan@one@point\hobby@quick}}
\makeatother

It is invoked via a to path:

\draw[red] (0,0) to[quick curve through={(1,1) (2,0) (3,0) (2,2)}]
(2,4);

And here's the comparison with the open version of the path in the question. The red path uses Hobby's algorithm. The green path uses this quick version. The blue path is the result of plot[smooth].

Quick version of Hobby's algorithm

Andrew Stacey
  • 153,724
  • 43
  • 389
  • 751
  • 4
    Your code is amazing. I didn't expect an implementation of the algorithm was possible in pure tex. But it looks like a nightmare to maintain... Was this code entirely written by hand? Or there is some kind of "compiler" to generate it? By the way, I voted your answer, but not mine :) – JLDiaz May 13 '12 at 10:25
  • Nope, the code was written entirely "by hand" ("the hand of Emacs"). It needs a severe re-write as it evolved considerably as I wrote it and discovered things about LaTeX3 that I never knew before. Oh, and I voted for your answer but not mine so I guess it balances out. – Andrew Stacey May 13 '12 at 18:11
  • 2
    (1) Your \int_compare:nTF { .. > 0 } { ... } { } can be changed to \int_compare:nT { .. > 0 } { ... }, omitting the empty false branch. (2) \exp_args:NNx \prop_put:Nnx => \prop_put:Nxx, with a \cs_generate_variant:Nn \prop_put:Nnn { Nxx } outside. (3) More generally, the consensus in the team is to avoid \exp_args:... in favor of generating the necessary variants, except perhaps if that variant is going to be used only once. (4) I'm curious to see how much code is saved by using the new l3fp (on the svn, will be on CTAN in July). (5) Impressive piece of work! – Bruno Le Floch May 24 '12 at 11:52
  • @BrunoLeFloch (1) Looks useful, thanks (I think there are many such one-sided tests). (2) Did that, thanks to Joseph's help understanding variants (ditto (3)). (4) Me too! (5) Thanks! (6) The current version has a "poor man's array" implementation. I'm half curious, half dreading what you (and the other L3 experts) will think of it. – Andrew Stacey May 24 '12 at 11:57
  • (7) About atan, it is actually quite difficult to implement: the Taylor series converges very slowly on [0,1], and there are no tricks to accelerate that much, so I will have to implement some argument reduction, but the details are not clear to me yet. IIRC, pgfmath simply has a table for that, which is impossible for LaTeX3 since I want 16 digits of precision. – Bruno Le Floch May 24 '12 at 11:57
  • (7) Ah, I see. That precision issue bit me - I was comparing the output of pgfmaths atan2 with l3fps idea of \pi and the two never agreed due to the precision difference. The Taylor series isn't necessarily the best approximation of a function - are there other standard approximations of atan? – Andrew Stacey May 24 '12 at 11:59
  • (6) (your 6) If I understand correctly, you are using props with integer keys? Then yes, I believe that this is the "standard" way. The other option if you are only going to access elements in order is to use a sequence, pushing items on one end, and perhaps popping them on the other (or the same). That woudl be faster than a prop. But for random access, a prop is best. – Bruno Le Floch May 24 '12 at 12:01
  • @BrunoLeFloch Yes, I am using a prop with integer keys. I'm defining a few auxiliary macros that use the fact that the keys are integers: push and pop, and the \array_map_function:NN steps through the array in numerical order rather than stored order (breaks expandability, though). I need random access as I need to get at values of different indices as I step through the array. – Andrew Stacey May 24 '12 at 12:02
  • @BrunoLeFloch (Just got the "move to chat" message so posted next message there instead) – Andrew Stacey May 24 '12 at 12:06
  • +1, especially for the quick version. Is it in a package somewhere? – Blair May 18 '13 at 08:39
  • @Blair it's the hobby package on CTAN, see link at top of answer. – Andrew Stacey May 18 '13 at 09:01
  • @AndrewStacey forgive me if I'm missing something, but that package seems to require LaTeX3. I'm quite happy copying the quick method in your last update, I was just curious if there was a package for the 2E compatible stuff. – Blair May 18 '13 at 11:05
  • @Blair It's all in one package. And the fact that it is uses LaTeX3 doesn't mean that you as the user do anything different. It can be used in a normal LaTeX2e document without modification. You're probably already using some LaTeX3 stuff without realising it. – Andrew Stacey May 18 '13 at 11:26
  • Forgive me for asking here, but after putting \usetikzlibrary{hobby} into my document I get the error "Undefined control sequence: __msg_kernel_new:nnn { kernel } { show-array }" on line 169 of pml3array.sty (fresh copy of hobby from CTAN). This is with both pdfLaTeX (1.40.13) and xeLaTeX (0.9998) from the Ubuntu repository copy of TexLive 2012. Hence my assumption that LaTeX3 couldn't be used alongside 2E. Am I missing a step? – Blair May 21 '13 at 01:09
  • @Blair It's fine to ask here. At the moment, it's best to think of LaTeX3 as just another package (or set of packages). It looks as though hobby wants a more up to date version of the LaTeX3 code than is in the Ubuntu repository. You could try updating your TeXLive. As an alternative, if you're happy with the quick version of the algorithm then edit the file pgflibraryhobby.code.tex and comment out the line \input{hobby.code.tex} – Andrew Stacey May 21 '13 at 07:45
52

Just for fun, I decided to implement Hobby's algorithm in pure Python (well, not pure, I had to use numpy module to solve a linear system of equations).

Currently, my code works on simple paths, in which all joins are "curved" (i.e: "..") and no directions are specified at the knots. However, tension can be specified at each segment, and even as a "global" value to apply to the whole path. The path can be cyclic or open, and in the later it is also possible to specify initial and final curl.

The module can be called from LaTeX, using python.sty package or even better, using the technique demonstrated by Martin in another answer to this same question.

Adapting Martin's code to this case, the following example shows how to use the python script:

\documentclass{minimal}
\usepackage{tikz}
\usepackage{xparse}

\newcounter{mppath} \DeclareDocumentCommand\mppath{ o m }{% \addtocounter{mppath}{1} \def\fname{path\themppath.tmp} \IfNoValueTF{#1} {\immediate\write18{python mp2tikz.py '#2' >\fname}} {\immediate\write18{python mp2tikz.py '#2' '#1' >\fname}} \input{\fname} }

\begin{document} \begin{tikzpicture}[scale=0.1] \mppath[very thick]{(0,0)..(60,40)..tension 2..(40,90)..(10,70)..(30,50)..cycle} \mppath[blue,tension=3]{(0,0)..(60,40)..(40,90)..(10,70)..(30,50)..cycle}; \end{tikzpicture} \end{document}

Note that options passed to mppath are en general tikz options, but two new options are also available: tension, which applies the given tension to all the path, and curl which applies the given curl to both ends of an open path.

Running the above example through pdflatex -shell-escape produces the following output:

output from latex example

The python code of this module is below. The details of the algorithm were obtained from the "METAFONT: The program" book. Currently the class design of the python code is prepared to deal with more complex kind of paths, but I did not have time to implement the part which breaks the path into "idependendty solvable" subpaths (this would be at knots which do not have smooth curvature, or at which the path changes from curved to straight). I tried to document the code as much as I can, so that anyone could improve it.

# mp2tikz.py
# (c) 2012 JL Diaz
#
# This module contains classes and functions to implement Jonh Hobby's
# algorithm to find a smooth curve which  passes through a serie of given
# points. The algorithm is used in METAFONT and MetaPost, but the source code
# of these programs is hard to read. I tried to implement it in a more 
# modern way, which makes the algorithm more understandandable and perhaps portable
# to other languages
#
# It can be imported as a python module in order to generate paths programatically
# or used from command line to convert a metapost path into a tikz one
#
# For the second case, the use is:
#
# $ python mp2tikz.py <metapost path> <options>
#
# Where:
#  <metapost path> is a path using metapost syntax with the following restrictions:
#    * All points have to be explicit (no variables or expressions)
#    * All joins have to be "curved" ( .. operator)
#    * Options in curly braces next to the nodes are ignored, except
#      for {curl X} at end points
#    * tension can be specified using metapost syntax
#    * "cycle" as end point denotes a cyclic path, as in metapost
#    Examples:
#      (0,0) .. (60,40) .. (40,90) .. (10,70) .. (30,50) .. cycle
#      (0,0) .. (60,40) .. (40,90) .. (10,70) .. (30,50)
#      (0,0){curl 10} .. (60,40) .. (40,90) .. (10,70) .. (30,50)
#      (0,0) .. (60,40) .. (40,90) .. tension 3 .. (10,70) .. (30,50) .. cycle
#      (0,0) .. (60,40) .. (40,90) .. tension 1 and 3 .. (10,70) .. (30,50) .. cycle
#
#  <options> can be:
#     tension = X. The given tension is applied to all segments in the path by default
#        (but tension given at specific points override this setting at those points)
#     curl = X. The given curl is applied by default to both ends of the open path
#        (but curl given at specific endings override this setting at that point)
#     any other options are considered tikz options.
#  
#   The script prints in standard output a tikz command which draws the given path
#   using the given options. In this path all control points are explicit, as computed
#   by the string using Hobby's algorith. 
#  
#   For example:
#
#   $ python mp2tikz.py "(0,0) .. (10,10) .. (20,0) .. (10, -10) .. cycle" "tension =3, blue"
#
#   Would produce
#   \draw[blue] (0.0000, 0.0000) .. controls (-0.00000, 1.84095) and (8.15905, 10.00000)..
#   (10.0000, 10.0000) .. controls (11.84095, 10.00000) and (20.00000, 1.84095)..
#   (20.0000, 0.0000) .. controls (20.00000, -1.84095) and (11.84095, -10.00000)..
#   (10.0000, -10.0000) .. controls (8.15905, -10.00000) and (0.00000, -1.84095)..(0.0000, 0.0000); 
#

from math import sqrt, sin, cos, atan2, atan, degrees, radians, pi

Coordinates are stored and manipulated as complex numbers,

so we require cmath module

import cmath

def arg(z): return atan2(z.imag, z.real)

def direc(angle): """Given an angle in degrees, returns a complex with modulo 1 and the given phase""" phi = radians(angle) return complex(cos(phi), sin(phi))

def direc_rad(angle): """Given an angle in radians, returns a complex with modulo 1 and the given phase""" return complex(cos(angle), sin(angle))

class Point(): """This class implements the coordinates of a knot, and all kind of auxiliar parameters to compute a smooth path passing through it""" z = complex(0,0) # Point coordinates alpha = 1 # Tension at point (1 by default) beta = 1 theta = 0 # Angle at which the path leaves phi = 0 # Angle at which the path enters xi = 0 # angle turned by the polyline at this point v_left = complex(0,0) # Control points of the Bezier curve at this point u_right = complex(0,0) # (to be computed later) d_ant = 0 # Distance to previous point in the path d_post = 0 # Distance to next point in the path

def __init__(self, z, alpha=1, beta=1, v=complex(0,0), u=complex(0,0)):
    &quot;&quot;&quot;Constructor. Coordinates can be given as a complex number
    or as a tuple (pair of reals). Remaining parameters are optional
    and take sensible default vaules.&quot;&quot;&quot;
    if type(z)==complex:
        self.z=z
    else:
        self.z=complex(z[0], z[1])
    self.alpha = alpha
    self.beta = beta
    self.v_left = v
    self.u_right = u
    self.d_ant  = 0
    self.d_post = 0
    self.xi   = 0
def __str__(self):
    &quot;&quot;&quot;Creates a printable representation of this object, for
    debugging purposes&quot;&quot;&quot;
    return &quot;&quot;&quot;    z=(%.3f, %.3f)  alpha=%.2f beta=%.2f theta=%.2f phi=%.2f

[v=(%.2f, %.2f) u=(%.2f, %.2f) d_ant=%.2f d_post=%.2f xi=%.2f]""" % (self.z.real, self.z.imag, self.alpha, self.beta, degrees(self.theta), degrees(self.phi), self.v_left.real, self.v_left.imag, self.u_right.real, self.u_right.imag, self.d_ant, self.d_post, degrees(self.xi))

class Path(): """This class implements a path, which is a list of Points""" p = None # List of points cyclic = True # Is the path cyclic? curl_begin = 1 # If not, curl parameter at endpoints curl_end = 1 def init(self, p, tension=1, cyclic=True, curl_begin=1, curl_end=1): self.p = [] for pt in p: self.p.append(Point(pt, alpha=1.0/tension, beta=1.0/tension)) self.cyclic = cyclic self.curl_begin = curl_begin self.curl_end = curl_end

def range(self):
    &quot;&quot;&quot;Returns the range of the indexes of the points to be solved.
    This range is the whole length of p for cyclic paths, but excludes
    the first and last points for non-cyclic paths&quot;&quot;&quot;
    if self.cyclic:
        return range(len(self.p))
    else:
        return range(1, len(self.p)-1)

# The following functions allow to use a Path object like an array
# so that, if x = Path(...), you can do len(x) and x[i]
def append(self, data):
    self.p.append(data)

def __len__(self):
    return len(self.p)

def __getitem__(self, i):
    &quot;&quot;&quot;Gets the point [i] of the list, but assuming the list is
    circular and thus allowing for indexes greater than the list
    length&quot;&quot;&quot;
    i %= len(self.p)
    return self.p[i]

# Stringfication
def __str__(self):
    &quot;&quot;&quot;The printable representation of the object is one suitable for
    feeding it into tikz, producing the same figure than in metapost&quot;&quot;&quot;
    r = []
    L = len(self.p)
    last = 1
    if self.cyclic:
        last = 0
    for k in range(L-last):
        post = (k+1)%L
        z = self.p[k].z
        u = self.p[k].u_right
        v = self.p[post].v_left
        r.append(&quot;(%.4f, %.4f) .. controls (%.5f, %.5f) and (%.5f, %.5f)&quot; %                        (z.real, z.imag, u.real, u.imag, v.real, v.imag))
    if self.cyclic:
        last_z = self.p[0].z
    else:
        last_z = self.p[-1].z
    r.append(&quot;(%.4f, %.4f)&quot; % (last_z.real, last_z.imag))
    return &quot;..&quot;.join(r)

def __repr__(self):
    &quot;&quot;&quot;Dumps internal parameters, for debugging purposes&quot;&quot;&quot;
    r = [&quot;Path information&quot;]
    r.append(&quot;Cyclic=%s, curl_begin=%s, curl_end=%s&quot; % (self.cyclic,
        self.curl_begin, self.curl_end))
    for pt in self.p:
        r.append(str(pt))
    return &quot;\n&quot;.join(r)

Now some functions from John Hobby and METAFONT book.

"Velocity" function

def f(theta, phi): n = 2+sqrt(2)(sin(theta)-sin(phi)/16)(sin(phi)-sin(theta)/16)(cos(theta)-cos(phi)) m = 3(1 + 0.5(sqrt(5)-1)cos(theta) + 0.5(3-sqrt(5))cos(phi)) return n/m

def control_points(z0, z1, theta=0, phi=0, alpha=1, beta=1): """Given two points in a path, and the angles of departure and arrival at each one, this function finds the appropiate control points of the Bezier's curve, using John Hobby's algorithm""" i = complex(0,1) u = z0 + cmath.exp(itheta)(z1-z0)f(theta, phi)alpha v = z1 - cmath.exp(-iphi)(z1-z0)f(phi, theta)beta return(u,v)

def pre_compute_distances_and_angles(path): """This function traverses the path and computes the distance between adjacent points, and the turning angles of the polyline which joins them""" for i in range(len(path)): v_post = path[i+1].z - path[i].z v_ant = path[i].z - path[i-1].z # Store the computed values in the Points of the Path path[i].d_ant = abs(v_ant) path[i].d_post = abs(v_post) path[i].xi = arg(v_post/v_ant) if not path.cyclic: # First and last xi are zero path[0].xi = path[-1].xi = 0 # Also distance to previous and next points are zero for endpoints path[0].d_ant = 0 path[-1].d_post = 0

def build_coefficients(path): """This function creates five vectors which are coefficients of a linear system which allows finding the right values of "theta" at each point of the path (being "theta" the angle of departure of the path at each point). The theory is from METAFONT book.""" A=[]; B=[]; C=[]; D=[]; R=[] pre_compute_distances_and_angles(path) if not path.cyclic: # In this case, first equation doesnt follow the general rule A.append(0) B.append(0) curl = path.curl_begin alpha_0 = path[0].alpha beta_1 = path[1].beta xi_0 = (alpha_02) * curl / (beta_12) xi_1 = path[1].xi C.append(xi_0alpha_0 + 3 - beta_1) D.append((3 - alpha_0)xi_0 + beta_1) R.append(-D[0]*xi_1)

# Equations 1 to n-1 (or 0 to n for cyclic paths)
for k in path.range():
    A.append(   path[k-1].alpha  / ((path[k].beta**2)  * path[k].d_ant))
    B.append((3-path[k-1].alpha) / ((path[k].beta**2)  * path[k].d_ant))
    C.append((3-path[k+1].beta)  / ((path[k].alpha**2) * path[k].d_post))
    D.append(   path[k+1].beta   / ((path[k].alpha**2) * path[k].d_post))
    R.append(-B[k] * path[k].xi  - D[k] * path[k+1].xi)

if not path.cyclic:
    # The last equation doesnt follow the general form
    n = len(R)     # index to generate
    C.append(0)
    D.append(0)
    curl = path.curl_end
    beta_n = path[n].beta
    alpha_n_1 = path[n-1].alpha
    xi_n = (beta_n**2) * curl / (alpha_n_1**2)
    A.append((3-beta_n)*xi_n + alpha_n_1)
    B.append(beta_n*xi_n + 3 - alpha_n_1)
    R.append(0)
return (A, B, C, D, R)

import numpy as np # Required to solve the linear equation system

def solve_for_thetas(A, B, C, D, R): """This function receives the five vectors created by build_coefficients() and uses them to build a linear system with N unknonws (being N the number of points in the path). Solving the system finds the value for theta (departure angle) at each point""" L=len(R) a = np.zeros((L, L)) for k in range(L): prev = (k-1)%L post = (k+1)%L a[k][prev] = A[k] a[k][k] = B[k]+C[k] a[k][post] = D[k] b = np.array(R) return np.linalg.solve(a,b)

def solve_angles(path): """This function receives a path in which each point is "open", i.e. it does not specify any direction of departure or arrival at each node, and finds these directions in such a way which minimizes "mock curvature". The theory is from METAFONT book."""

# Basically it solves
# a linear system which finds all departure angles (theta), and from
# these and the turning angles at each point, the arrival angles (phi)
# can be obtained, since theta + phi + xi = 0  at each knot&quot;&quot;&quot;
x = solve_for_thetas(*build_coefficients(path))
L = len(path)
for k in range(L):
    path[k].theta = x[k]
for k in range(L):
    path[k].phi = - path[k].theta - path[k].xi

def find_controls(path): """This function receives a path in which, for each point, the values of theta and phi (leave and enter directions) are known, either because they were previously stored in the structure, or because it was computed by function solve_angles(). From this path description this function computes the control points for each knot and stores it in the path. After this, it is possible to print path to get a string suitable to be feed to tikz.""" r = [] for k in range(len(path)): z0 = path[k].z z1 = path[k+1].z theta = path[k].theta phi = path[k+1].phi alpha = path[k].alpha beta = path[k+1].beta u,v=control_points(z0, z1, theta, phi, alpha, beta) path[k].u_right = u path[k+1].v_left = v

def mp_to_tikz(path, command=None, options=None): """Utility funcion which receives a string containing a metapost path and uses all the above to generate the tikz version with explicit control points. It does not make a full parsing of the metapost path. Currently it is not possible to specify directions nor tensions at knots. It uses default tension = 1, default curl =1 for both ends in non-cyclic paths and computes the optimal angles at each knot. It does admit however cyclic and non-cyclic paths. To summarize, the only allowed syntax is z0 .. z1 .. z2, where z0, z1, etc are explicit coordinates such as (0,0) .. (1,0) etc.. And optionally the path can ends with the literal "cycle".""" tension = 1 curl = 1 if options: opt = [] for o in options.split(","): o=o.strip() if o.startswith("tension"): tension = float(o.split("=")[1]) elif o.startswith("curl"): curl = float(o.split("=")[1]) else: opt.append(o) options = ",".join(opt) new_path = mp_parse(path, default_tension = tension, default_curl = curl) # print repr(new_path) solve_angles(new_path) find_controls(new_path) if command==None: command="draw" if options==None: options = "" else: options = "[%s]" % options return "\%s%s %s;" % (command, options, str(new_path))

def mp_parse(mppath, default_tension = 1, default_curl = 1): """This function receives a string which contains a path in metapost syntax, and returns a Path object which stores the same path in the structure required to compute the control points. The path should only contain explicit coordinates and numbers. Currently only "curl" and "tension" keywords are understood. Direction options are ignored.""" if mppath.endswith(";"): # Remove last semicolon mppath=mppath[:-1] pts = mppath.split("..") # obtain points pts = [p.strip() for p in pts] # remove extra spaces

if pts[-1] == &quot;cycle&quot;:
    is_cyclic = True
    pts=pts[:-1]     # Remove this last keyword
else:
    is_cyclic = False
path = Path([], cyclic=is_cyclic)
path.curl_begin = default_curl
path.curl_end   = default_curl
alpha = beta = 1.0/default_tension
k=0
for p in pts:
    if p.startswith(&quot;tension&quot;):
        aux = p.split()
        alpha = 1.0/float(aux[1])
        if len(aux)&gt;3:
            beta = 1.0/float(aux[3])
        else:
            beta = alpha
    else:
        aux = p.split(&quot;{&quot;)  # Extra options at the point
        p = aux[0].strip()
        if p.startswith(&quot;curl&quot;):
            if k==0:
                path.curl_begin=float(aux[1])
            else:
                path.curl_end = float(aux[1])
        elif p.startswith(&quot;dir&quot;):
            # Ignored by now
            pass

        path.append(Point(eval(p)))  # store the pair of coordinates
        # Update tensions
        path[k-1].alpha = alpha
        path[k].beta  = beta
        alpha = beta = 1.0/default_tension
        k = k + 1
if is_cyclic:
    path[k-1].alpha = alpha
    path[k].beta = beta
return path

def main(): """Example of conversion. Takes a string from stdin and outputs the result in stdout. """ import sys if len(sys.argv)>2: opts = sys.argv[2] else: opts = None path = sys.argv[1] print mp_to_tikz(path, options = opts)

if name == "main": main()

Update

The code supports now tension at each segment, or as a global option for the path. Also changed the way of calling it from latex, using Martin's technique.

trujello
  • 597
JLDiaz
  • 55,732
  • 3
    Posting the python code is absolutely fine. – Andrew Stacey May 10 '12 at 10:30
  • Wow, very nice! Could this be embedded directly in the TeX file using the python package? – Jake May 10 '12 at 11:40
  • Yes! It is possible. I edited the answer to show an example – JLDiaz May 10 '12 at 16:23
  • @AndrewStacey, thanks. I edited the answer, and removed my concerns. – JLDiaz May 10 '12 at 16:36
  • 1
    Many, many thanks for this answer! It was invaluable in debugging my own version. – Andrew Stacey May 12 '12 at 19:59
  • 1
    This was the main reason for me to write the python code. To help somebody else, more tex-fluent then me, to create the pure tex implementation. But frankly, I didn't expect it was feasible. Kudos for you! – JLDiaz May 13 '12 at 10:22
  • @JLDiaz I too have been using you version as a debugging tool. I have noticed a problem with this path: "(0, 0) .. (1,0) .. cycle" It generates the following (0.0000, 0.0000) .. controls (0.11965, 0.20723) and (1.60422, -1.04653)..(1.0000, 0.0000) .. controls (0.88035, 0.20723) and (-0.60422, -1.04653)..(0.0000, 0.0000) MetaPost on the other hand gives: (0,0)..controls (0,-0.66667) and (1,-0.66667) ..(1,0)..controls (1,0.66667) and (0,0.66667) ..cycle – soegaard Aug 05 '13 at 16:21
  • Is the solution to special case cycles with two points? – soegaard Aug 05 '13 at 16:21
  • For anyone who needs the code: I wrote a clean, modern Python implementation of the Hobby algorithm. I learned that the code in the above post is Python 2 and uses many Python language features incorrectly. – trujello Oct 16 '21 at 20:58
37

** Update 12 May 2012**

Now, the syntax is usable directly inside a \draw command. It can parse any coordinate legal in tikz (that is a polar coordinate, a node, etc.). The issue with unit is solved. Note that now, I parse the ps output.

-- Taken from luamplib
local mpkpse = kpse.new('luatex', 'mpost')

local function finder(name, mode, ftype)
   if mode == "w" then
  return name
   else
  return mpkpse:find_file(name,ftype)
   end
end

local lpeg = require('lpeg')

local P, S, R, C, Cs, Ct = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cs, lpeg.Ct

function parse_mp_tikz_path(s)
   local space = S(' \n\t')
   local ddot = space^0 * P('..') * space^0
   local cycle = space^0 * P('cycle') * space^0

   local path = Ct((C((1 - ddot)^1) * ddot)^1 * cycle)  / function (t) local s = '' for i = 1,#t do s = s .. string.format('\\tikz@scan@one@point\\pgfutil@firstofone%s\\relax\\edef\\temp{\\temp (\\the\\pgf@x,\\the\\pgf@y) ..}',t[i]) end return s .. '\\xdef\\temp{\\temp  cycle}' end
   return tex.sprint(luatexbase.catcodetables.CatcodeTableLaTeXAtLetter,lpeg.match(Cs(path),s))
end

local function parse_ps(s)
   local newpath = P('newpath ')
   local closepath = P(' closepath')
   local path_capture = (1 - newpath)^0 * newpath * C((1 - closepath)^0) * closepath * true
   return lpeg.match(path_capture,s)
end

local function parse_path(s)
   local digit = R('09')
   local dot = P('.')
   local minus = P('-')
   local float = minus^0 * digit^1 * (dot * digit^1)^-1

   local space = P(' ')
   local newline = P('\n')

   local coord = Ct(C(float) * space^1 * C(float)) / function (t) return string.format('(%spt,%spt)',t[1],t[2]) end

   local moveto = coord * (P(' moveto') * newline^-1 / '')
   local curveto = Ct(Cs(coord) * space^1 * Cs(coord) * space^1 * Cs(coord) * P(' curveto') * newline^-1) / function (t) return string.format(' .. controls %s and %s .. %s',t[1], t[2], t[3]) end 
   local path = (Cs(moveto) + Cs(curveto))^1

   return lpeg.match(Cs(path),s)
end

function getpathfrommp(s)
   local mp = mplib.new({
               find_file = finder,
               ini_version = true,})
   mp:execute(string.format('input %s ;', 'plain'))
   local rettable = mp:execute('beginfig(1) draw ' .. s .. '; endfig;end;')
   if rettable.status == 0 then
  local ps = rettable.fig[1]:postscript()
  local ps_parsed = parse_ps(ps)
  local path_parsed = parse_path(ps_parsed)
  return tex.sprint(path_parsed)
   end
end

And the TeX file.

\documentclass{standalone}

\usepackage{luatexbase-cctb}

\usepackage{tikz}

\directlua{dofile('mplib-se.lua')}

\def\getpathfrommp#1{%
  \pgfextra{\def\temp{}\directlua{parse_mp_tikz_path('#1')}}
  \directlua{getpathfrommp('\temp')}}

\begin{document}

\begin{tikzpicture}
  \coordinate (A) at (6,4);
  \draw \getpathfrommp{(0,0) .. (A) .. (4,9) .. (1,7)
    .. (3,5) .. cycle};
\end{tikzpicture}

\end{document}

Here is a "poor man hobby algorithm" approach, assuming the use of luatex is allowed.

luatex comes with an embedded metapost library. So we can ask the library to do the job, then parse the output and give it back to tikz.

AFAIU, two kind of output could be parsed: the postscript one and the svg one. I chose the svg and use the svg.path tikz library to render the computed path.

First the lua file (to be saved as mplib-se.lua):

-- Taken from luamplib
local mpkpse = kpse.new('luatex', 'mpost')

local function finder(name, mode, ftype)
   if mode == "w" then
  return name
   else
  return mpkpse:find_file(name,ftype)
   end
end

function getpathfrommp(s)
   local mp = mplib.new({
            find_file = finder,
            ini_version = true,})
   mp:execute(string.format('input %s ;', 'plain'))
   local rettable = mp:execute('beginfig(1) draw' .. s .. '; endfig;end;')
   if rettable.status == 0 then
  local path = rettable.fig[1]:svg()
  local path_patt, match_quotes = 'path d=".-"', '%b""'
  return tex.sprint(string.gsub(string.match(string.match(path, path_patt),match_quotes),'"',''))
   end
end

Then the tex file itself.

\documentclass{standalone}

\usepackage{tikz}
\usetikzlibrary{svg.path}

\directlua{dofile('mplib-se.lua')}

\def\pgfpathsvggetpathfrommp#1{%
  \expandafter\pgfpathsvg\expandafter{%
    \directlua{getpathfrommp('#1')}}}

\begin{document}

\begin{tikzpicture}
  \pgfpathsvggetpathfrommp{(0,0) .. (60,40) .. (40,90) .. (10,70)
    .. (30,50) .. cycle}
  \pgfusepath{stroke}
  \begin{scope}[scale=.1,draw=red]
    \draw (0, 0) .. controls (5.18756, -26.8353) and (60.36073, -18.40036)
    .. (60, 40) .. controls (59.87714, 59.889) and (57.33896, 81.64203)
    .. (40, 90) .. controls (22.39987, 98.48387) and (4.72404, 84.46368)
    .. (10, 70) .. controls (13.38637, 60.7165) and (26.35591, 59.1351)
    .. (30, 50) .. controls (39.19409, 26.95198) and (-4.10555, 21.23804)
    .. (0, 0);    
 \end{scope}
\end{tikzpicture}

\end{document}

And the result. Note that there must be some kind of unit mismatch.

enter image description here


Update

Here is another version, using lpeg to parse the svg code. This way, one can scale the output of metapost to fit the correct unit.

-- Taken from luamplib
local mpkpse = kpse.new('luatex', 'mpost')

local function finder(name, mode, ftype)
   if mode == "w" then
  return name
   else
  return mpkpse:find_file(name,ftype)
   end
end

local lpeg = require('lpeg')

local P, S, R, C, Cs = lpeg.P, lpeg.S, lpeg.R, lpeg.C, lpeg.Cs

local function parse_svg(s)
   local path_patt = P('path d="')
   local path_capture = (1 - path_patt)^0 * path_patt * C((1 - P('"'))^0) * P('"') * (1 - P('</svg>'))^0 * P('</svg>')
   return lpeg.match(path_capture,s)
end

local function parse_path_and_convert(s)
   local digit = R('09')
   local comma = P(',')
   local dot = P('.')
   local minus = P('-')
   local float = C(minus^0 * digit^1 * dot * digit^1) / function (s) local x = tonumber(s)/28.3464567 return tostring(x - x%0.00001) end

   local space = S(' \n\t')

   local coord = float * space * float

   local moveto = P('M') * coord
   local curveto = P('C') * coord * comma * coord * comma * coord
   local path = (moveto + curveto)^1 * P('Z') * -1

   return lpeg.match(Cs(path),s)
end

function getpathfrommp(s)
   local mp = mplib.new({
    find_file = finder,
    ini_version = true,})
   mp:execute(string.format('input %s ;', 'plain'))
   local rettable = mp:execute('beginfig(1) draw' .. s .. '; endfig;end;')
   if rettable.status == 0 then
  local svg = rettable.fig[1]:svg()
  return tex.sprint(parse_path_and_convert(parse_svg(svg)))
   end
end
cjorssen
  • 10,032
  • 4
  • 36
  • 126
  • 2
    Note that now that i have a parser for the svg output, i no longer need the svg lib. I could just translate the output to something readable by tikz. Maybe tomorrow... – cjorssen May 08 '12 at 18:03
29

This isn't a complete PS parser (or even close to that) but it does parse your example and could easily be extended. So it allows you just to drop the metapost generated postscript in to the document.

enter image description here

\documentclass{standalone}
\usepackage{tikz}
\begin{document}
\begin{tikzpicture}[scale=0.1] 
\draw (0, 0) .. controls (5.18756, -26.8353) and (60.36073, -18.40036)
   .. (60, 40) .. controls (59.87714, 59.889) and (57.33896, 81.64203)
   .. (40, 90) .. controls (22.39987, 98.48387) and (4.72404, 84.46368)
   .. (10, 70) .. controls (13.38637, 60.7165) and (26.35591, 59.1351)
   .. (30, 50) .. controls (39.19409, 26.95198) and (-4.10555, 21.23804)
   .. (0, 0);    
\end{tikzpicture}

---

\def\hmm{%
\def\hmmstack{}%
\def\hmmtikz{}%
\hmmx}

\def\hmmx#1 {%
  \def\tmp{#1}%
  \ifx\tmp\hmmnewpath\xhmmnewpath\fi
  \ifx\tmp\hmmmoveto\xhmmmoveto\fi
  \ifx\tmp\hmmcurveto\xhmmcurveto\fi
  \ifx\tmp\hmmclosempath\xhmmclosepath\fi
  \ifx\tmp\hmmstroke\xhmmstroke\fi
  \ifx\tmp\hmmend\xhmmend\fi
  \hmmpush
  \hmmx}

\def\hmmpush{%
  \edef\hmmstack{\tmp\space\hmmstack}}

\def\hmmpop#1{%
  \expandafter\xhmmpop\hmmstack\@nil#1}

\def\xhmmpop#1 #2\@nil#3{%
  \def#3{#1}%
  \def\hmmstack{#2}}

\def\hmmnewpath{newpath}
\def\xhmmnewpath#1\hmmx{\fi\edef\hmmtikz{}\hmmx}

\def\hmmmoveto{moveto}
\def\xhmmmoveto#1\hmmx{\fi
\hmmpop\hmma
\hmmpop\hmmb
\edef\hmmtikz{\hmmtikz\space(\hmma,\hmmb)}\hmmx}

\def\hmmcurveto{curveto}
\def\xhmmcurveto#1\hmmx{\fi
\hmmpop\hmma
\hmmpop\hmmb
\hmmpop\hmmc
\hmmpop\hmmd
\hmmpop\hmme
\hmmpop\hmmf
\edef\hmmtikz{\hmmtikz\space.. controls (\hmmf,\hmme) and (\hmmd,\hmmc) .. (\hmmb,\hmma)}\hmmx}

\def\hmmend{\end{hmm}}
\def\xhmmend#1\hmmx{\fi
\begin{tikzpicture}[scale=0.1] 
\expandafter\draw \hmmtikz;\end{tikzpicture}
\end{hmm}}

\begin{hmm}
newpath 0 0 moveto
5.18756 -26.8353 60.36073 -18.40036 60 40 curveto
59.87714 59.889 57.33896 81.64203 40 90 curveto
22.39987 98.48387 4.72404 84.46368 10 70 curveto
13.38637 60.7165 26.35591 59.1351 30 50 curveto
39.19409 26.95198 -4.10555 21.23804 0 0 curveto
closepath stroke
\end{hmm}

\end{document}
David Carlisle
  • 757,742
18

Another, pretty simple approach is to use Asymptote which also supports Metapost's path syntax. When printing a path using its write function, we get the expanded path containing the Bézier control points. The following small Perl script wraps the call of asymptote and tweaks the output accordingly:

$path = $ARGV[0];
$pathstr = `echo 'path p=$path; write(p);'|asy`;     # get expanded path
$pathstr =~ s/^(\([^)]+\))(.*)cycle\s*$/\1\2\1/s;    # replace 'cycle' with initial point
$pathstr =~ s/(\d+\.\d{6,})/sprintf('%.5f', $1)/esg; # reduce number of decimal places
print <<EOF
\\begin{tikzpicture}[scale=0.1] 
\\draw $pathstr;
\\end{tikzpicture}
EOF

When calling the script with perl path2tikz.pl "(0,0)..(60,40)..(40,90)..(10,70)..(30,50)..cycle" it produces the following output:

\begin{tikzpicture}[scale=0.1] 
\draw (0,0).. controls (5.18756,-26.83529) and (60.36074,-18.40037)
 ..(60,40).. controls (59.87715,59.88901) and (57.33896,81.64203)
 ..(40,90).. controls (22.39986,98.48387) and (4.72403,84.46369)
 ..(10,70).. controls (13.38637,60.71651) and (26.35591,59.13511)
 ..(30,50).. controls (39.19409,26.95199) and (-4.10555,21.23803)
 ..(0,0);
\end{tikzpicture}

Calling the script from LaTeX

It's also possible to call the script from within a LaTeX document using \write18 (--escape-shell required). To do so, I use the following modified version that only prints a \draw statement without the surrounding tikzpicture environment:

$path = $ARGV[0];
$opt = $ARGV[1];
$pathstr = `echo 'path p=$path; write(p);'|asy`;     # get expanded path
$pathstr =~ s/^(\([^)]+\))(.*)cycle\s*$/\1\2\1/s;    # replace 'cycle' with initial point
$pathstr =~ s/(\d+\.\d{6,})/sprintf('%.5f', $1)/esg; # reduce decimal places
print "\\draw [$opt] $pathstr;";

The following sample document defines a macro \mpdraw that takes the Metapost path description and optional style parameters passed to PGF's \draw command.

\documentclass{standalone}
\usepackage{tikz}
\usepackage{xparse}

\newcounter{mppath}
\DeclareDocumentCommand\mppath{ o m }{%
   \addtocounter{mppath}{1}
   \def\fname{path\themppath.tmp}
   \IfNoValueTF{#1}
      {\immediate\write18{perl mp2tikz.pl '#2' >\fname}}
      {\immediate\write18{perl mp2tikz.pl '#2' '#1' >\fname}}
   \input{\fname}
}

\begin{document}
\begin{tikzpicture}[scale=0.1]
\mppath{(0,0)..(60,40)..(40,90)..(10,70)..(30,50)..cycle}
\mppath[fill=blue!20,style=dotted]{(0,0)..(60,40)..tension 2 ..(40,90)..tension 10 ..(10,70)..(30,50)..cycle}
\end{tikzpicture}
\end{document}
Martin
  • 2,628
  • 1
    Cue the daleks: "truncate, truncate.". Is there really a need for (**counts rapidly**) 12 decimal places? (Nice idea, for all that) – Andrew Stacey May 10 '12 at 13:11
  • No, definitely not. That's just a first simple "proof of concept" using the default output delivered by Asymptote. It shouldn't be that complicated to simplify/improve the output further. – Martin May 10 '12 at 13:17
  • 2
    I've added a statement to the script that reduces the number of decimal places to 5. – Martin May 10 '12 at 14:06
  • Great! (In case it wasn't clear, I think this is a good answer - I was being extremely picky.) – Andrew Stacey May 10 '12 at 18:36
  • @Andrew, thanks for your comment. Your previous note about the decimal places was absolutely valid (and easy to address). So, please keep being picky to point to things that could be improved. ;) – Martin May 10 '12 at 19:05
14

A convenient interpreter of meta*o*t path syntax is (not surprisingly) metapost itself, so I get metapost to parse its own picture structures and output a file in pgf format. This can then be \input within a tikzpicture or cut and paste, etc. In terms of workflow, this is somewhere between having it completely within the .tex file and transferring edited postscript paths across. Here is my mp2pgf.mp file (it includes its own sample code)

%   mp2pgf.mp
%   Metapost code to output paths etc in pgf format for use in a tikzpicture.
%   By Andrew Kepert, University of Newcastle, Australia
%   Released into the public domain by the author, but fixes/feedback would be nice.
%   Version: 20120823   (tidied up to post to tex.stackexchange.com)
%   
%   Usage: probably the easiest way:
%   input mp2pgf
%   beginfig(1)
%       (some metapost drawing commands)
%       pgf_write(currentpicture);
%   endfig;
%
%   Bugs: doesn't yet handle text, dash patterns, bounding boxes, transforms, glyphs, ...

% -- file name handling
string pgf_fname;
def pgf_open(expr fname)=
    if known pgf_fname: pgf_close; fi
    if string(fname) and (length(fname)>0): pgf_fname:=fname;
    else:       pgf_fname:=jobname if known charcode:&"-"&decimal(charcode) fi &".pgf"; fi
    write "% pgf code fragment generated by mp2pgf from metapost job "&jobname
        &" at "&decimal(hour)&":"&substring(1,3) of decimal(100+minute)
        &" on "&decimal(day)&"/"&decimal(month)&"/"&decimal(year)
        to pgf_fname;
    enddef;
def pgf_close=
    write EOF to pgf_fname;
    pgf_fname:=begingroup save $; string $; $ endgroup;
    enddef;

% -- decomposing pictures
def pgf_write(expr $)=
    % $ is a picture or a path.
    if not known pgf_fname: pgf_open(""); fi
    if picture $:
        if (length($)>1):
            for $$ within $:  pgf_write($$); endfor
        elseif stroked $: 
            if length dashpart $ > 0:
                message "WARNING: pgf output of dashed paths not implemented";
            fi
            write "% Stroked "&if cycle(pathpart $): "cycle" else: "path" fi 
                &" of length "&decimal(length pathpart $) to pgf_fname;
            write "\draw"&
                if iscoloured($): "[color=" & colourtopgf($) &"]"& fi
                " "&pathtopgf(pathpart $) & ";" to pgf_fname;
        elseif filled $:
            write "% Filled "&if cycle(pathpart $): "cycle" else: "path" fi 
                &" of length "&decimal(length pathpart $) to pgf_fname;
            write "\fill"&
                if iscoloured($): "[color=" & colourtopgf($) &"]"& fi
                " "&pathtopgf(pathpart $) & ";" to pgf_fname;
        elseif textual $:
            message "WARNING: pgf output of text objects not implemented";
        fi
    elseif path $:
        write pathtopgf(pathpart $) to pgf_fname;
    fi
    enddef;


% -- converting colours
def iscoloured(expr $)=
    (((redpart $)>0) or ((greenpart $)>0) or ((bluepart $)>0))
    enddef;
def colourtopgf(expr $)=
    begingroup save r,g,b,k;
    r=redpart $; g=greenpart $; b=bluepart $; k=1-r-g-b;
    "{rgb:black,"&decimal(k)& 
        if r>0:";red,"&decimal(r)& fi
        if g>0:";green,"&decimal(g)& fi
        if b>0:";blue,"&decimal(b)& fi "}"
    endgroup
    enddef;

% -- converting paths
def pairtopgf(expr $)=
    "("&decimal(xpart $)&"pt,"&decimal(ypart $)&"pt)"
    enddef;
def isstraight (expr p)=
    begingroup save a,b,c,d,e; pair a,b,c,d,e;
    a=point 0 of p;
    b=postcontrol 0 of p - a;
    c=precontrol 1 of p - a;
    d=point 1 of p - a;
    e=unitvector(d) yscaled -1;
    (abs(ypart(b zscaled e))<8eps) and (abs(ypart(c zscaled e))<8eps)
    endgroup
    enddef;
def pathtopgf(expr $)=
    begingroup
    save i,n,x,y;
    n=length $;
    for i = 0 upto n:
        z.ptof[i]=point i of $;
        z.prec[i]=precontrol i of $;
        z.postc[i]=postcontrol i of $;
    endfor
    for i = 0 upto length($)-1:
        pairtopgf(point i of $) &
        if isstraight(subpath(i,i+1) of $):"--"
        else: " .. controls "&pairtopgf(postcontrol i of $)&" and "&pairtopgf(precontrol i+1 of $)&" .. "
        fi &
    endfor
    pairtopgf(point n of $)
    if cycle($): & "-- cycle" fi
    endgroup
    enddef;


%-------------------------------------------------------------------------------------
%%%% If this file is being run as a stand-alone job, run the sample code.
%%%% Otherwise, bail out here.
if jobname="mp2pgf": else: endinput; fi


%%%%%%%% SAMPLE CODE %%%%%%%%%

beginfig(1)
    draw (0,0) .. (60,40) .. (40,90) .. (10,70) .. (30,50) .. cycle;
    draw unitsquare scaled 20 rotated 45 shifted (75,0) ;
    draw (100,0) -- (120,10) -- (100,20) -- (120,30) withcolor blue;
    fill fullcircle scaled 20 shifted (90,70) withcolor .5green;
    draw (100,0){up}..{left}(0,100) dashed evenly;
    draw btex ${d\over dx} x^2 = 2x$ etex shifted (60,90);
    %  pgf_open("sample.pgf");
    pgf_write(currentpicture);
    %  pgf_close;
endfig;
end


% http://tex.stackexchange.com/questions/54771/curve-through-a-sequence-of-points-with-metapost-and-tikz