diff options
Diffstat (limited to 'graphics/pgf/contrib/hobby/hobby_code.dtx')
-rw-r--r-- | graphics/pgf/contrib/hobby/hobby_code.dtx | 3614 |
1 files changed, 3614 insertions, 0 deletions
diff --git a/graphics/pgf/contrib/hobby/hobby_code.dtx b/graphics/pgf/contrib/hobby/hobby_code.dtx new file mode 100644 index 0000000000..03eb60574c --- /dev/null +++ b/graphics/pgf/contrib/hobby/hobby_code.dtx @@ -0,0 +1,3614 @@ +% \iffalse meta-comment +%<*internal> +\iffalse +%</internal> +%<*readme> +---------------------------------------------------------------- +hobby --- a TikZ/PGF library for drawing smooth(ish) curves using + Hobby's algorithm (implemented in LaTeX3) +E-mail: loopspace@mathforge.org +Released under the LaTeX Project Public License v1.3c or later +See http://www.latex-project.org/lppl.txt +---------------------------------------------------------------- + +This package defines a path generation function for TikZ/PGF +which implements Hobby's algorithm for a path built out of Bezier +curves which passes through a given set of points. + +The implementation is in LaTeX3. It can be used as as a TikZ +`to path`. +%</readme> +%<*internal> +\fi +\def\nameofplainTeX{plain} +\ifx\fmtname\nameofplainTeX\else + \expandafter\begingroup +\fi +%</internal> +%<*install> +\input docstrip.tex +\keepsilent +\askforoverwritefalse +\preamble +---------------------------------------------------------------- +hobby --- a TikZ/PGF library for drawing smooth(ish) curves using + Hobby's algorithm (implemented in LaTeX3) +E-mail: loopspace@mathforge.org +Released under the LaTeX Project Public License v1.3c or later +See http://www.latex-project.org/lppl.txt +---------------------------------------------------------------- + +\endpreamble +\postamble + +Copyright (C) 2012-2021 by Andrew Stacey <loopspace@mathforge.org> + +This file may be distributed and/or modified under the conditions +of the LaTeX Project Public License, either version 1.3 of this +license or (at your option) any later version. +The latest version of this license is in: + + http://www.latex-project.org/lppl.txt + +and version 1.3 or later is part of all distributions of LaTeX +version 2005/12/01 or later. + +This work is "maintained" (as per LPPL maintenance status) by +Andrew Stacey. + +This work consists of the files hobby.dtx + hobby_doc.tex +and the derived files hobby.code.tex + pgflibraryhobby.code.tex + tikzlibraryhobby.code.tex + pml3array.sty + hobby-l3draw.sty + hobby.ins + hobby.pdf + hobby_code.pdf + README.txt + +\endpostamble +\usedir{tex/latex/hobby} +\generate{\file{tikzlibraryhobby.code.tex} {\from{hobby.dtx}{tikzlibrary}}} +\generate{\file{pgflibraryhobby.code.tex} {\from{hobby.dtx}{pgflibrary}}} +\generate{\file{hobby.code.tex} {\from{hobby.dtx}{hobby}}} +\generate{\file{pml3array.sty} {\from{hobby.dtx}{array}}} +\generate{\file{hobby-l3draw.sty} {\from{hobby.dtx}{l3hobby}}} +%</install> +%<install>\endbatchfile +%<*internal> +\usedir{source/latex/hobby} +\generate{ + \file{\jobname.ins}{\from{\jobname.dtx}{install}} +} +\nopreamble\nopostamble +\generate{ + \file{README.txt}{\from{\jobname.dtx}{readme}} +} +\ifx\fmtname\nameofplainTeX + \expandafter\endbatchfile +\else + \expandafter\endgroup +\fi +%</internal> +%<*driver> +\documentclass[full]{l3doc} +\usepackage[T1]{fontenc} +\usepackage{csquotes} +\usepackage{lmodern} +\usepackage{tikz} +\usepackage{amsmath} +\usetikzlibrary{hobby,decorations.pathreplacing} +\usepackage[margin=3cm]{geometry} +\EnableCrossrefs +\CodelineIndex +\RecordChanges +\addtolength{\hoffset}{.4in} +\begin{document} + \DocInput{\jobname.dtx} +\end{document} +%</driver> +% \fi +% +% \CheckSum{3427} +% +% \CharacterTable +% {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z +% Lower-case \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z +% Digits \0\1\2\3\4\5\6\7\8\9 +% Exclamation \! Double quote \" Hash (number) \# +% Dollar \$ Percent \% Ampersand \& +% Acute accent \' Left paren \( Right paren \) +% Asterisk \* Plus \+ Comma \, +% Minus \- Point \. Solidus \/ +% Colon \: Semicolon \; Less than \< +% Equals \= Greater than \> Question mark \? +% Commercial at \@ Left bracket \[ Backslash \\ +% Right bracket \] Circumflex \^ Underscore \_ +% Grave accent \` Left brace \{ Vertical bar \| +% Right brace \} Tilde \~} +% +% +% +% \DoNotIndex{\newcommand,\newenvironment} +% +% \providecommand*{\url}{\texttt} +% \title{The \textsf{Hobby} package: code} +% \author{Andrew Stacey \\ \url{loopspace@mathforge.org}} +% \date{\hobbyVersion\ from\ \hobbyDate} +% \maketitle +% +% +% \StopEventually{\PrintChanges} +% \section{Implementation} +% +% \subsection{Main Code} +% +% \iffalse +%<*hobby> +% \fi +% +% We use \LaTeX3 syntax so need to load the requisite packages +% \begin{macrocode} +\RequirePackage{pml3array} +\ExplSyntaxOn +% \end{macrocode} +% +% \begin{macrocode} +\cs_generate_variant:Nn \fp_set:Nn {Nx} +\cs_generate_variant:Nn \tl_if_eq:nnTF {VnTF} +\cs_generate_variant:Nn \tl_if_eq:nnTF {xnTF} +% \end{macrocode} +% +% \subsubsection{Initialisation} +% +% We declare all our variables. +% +% Start with version and date, together with a check to see if we've been loaded twice (fail gracefully if so). +% +% \begin{macrocode} +\tl_clear:N \l_tmpa_tl +\tl_if_exist:NT \g__hobby_version +{ + \tl_set:Nn \l_tmpa_tl { + \ExplSyntaxOff + \tl_clear:N \l_tmpa_tl + \endinput + } +} +\tl_use:N \l_tmpa_tl + +\tl_new:N \g__hobby_version +\tl_new:N \g__hobby_date +\tl_gset:Nn \g__hobby_version {1.10} +\tl_gset:Nn \g__hobby_date {2023-08-30} +\DeclareDocumentCommand \hobbyVersion {} +{ + \tl_use:N \g__hobby_version +} +\DeclareDocumentCommand \hobbyDate {} +{ + \tl_use:N \g__hobby_date +} +% \end{macrocode} +% +% The function for computing the lengths of the control points depends on three parameters. +% These are set to \(a = \sqrt{2}\), \(b = 1/16\), and \(c = \frac{3 - \sqrt{5}}{2}\). +% \begin{macrocode} +\fp_new:N \g_hobby_parama_fp +\fp_new:N \g_hobby_paramb_fp +\fp_new:N \g_hobby_paramc_fp +\fp_gset:Nn \g_hobby_parama_fp {2^.5} +\fp_gset:Nn \g_hobby_paramb_fp {1/16} +\fp_gset:Nn \g_hobby_paramc_fp {(3-5^.5)/2} +% \end{macrocode} +% +% Now we define our objects for use in generating the path. +% +% \begin{macro}{\l_hobby_closed_bool} +% \Verb+\l_hobby_closed_bool+ is \Verb+true+ if the path is closed. +% \begin{macrocode} +\bool_new:N \l_hobby_closed_bool +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_disjoint_bool} +% \Verb+\l_hobby_disjoint_bool+ is \Verb+true+ if the path should start with a \Verb+moveto+ command. +% \begin{macrocode} +\bool_new:N \l_hobby_disjoint_bool +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_save_aux_bool} +% \Verb+\l_hobby_save_aux_bool+ is \Verb+true+ if when saving paths then they should be saved to the \Verb+aux+ file. +% \begin{macrocode} +\bool_new:N \l_hobby_save_aux_bool +\bool_set_true:N \l_hobby_save_aux_bool +\DeclareDocumentCommand \HobbyDisableAux {} +{ + \bool_set_false:N \l_hobby_save_aux_bool +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_points_array} +% \Verb+\g__hobby_points_array+ is an array holding the specified points on the path. +% In the \LaTeX3 code, a ``point'' is a token list of the form \Verb+<number>, <number>+. +% This gives us the greatest flexibility in passing points back and forth between the \LaTeX3 code and any calling code. +% The array is indexed by integers beginning with \(0\). +% In the documentation, we will use the notation \(z_k\) to refer to the \(k\)th point. +% \begin{macrocode} +\array_new:N \g__hobby_points_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_points_x_array} +% \Verb+\g__hobby_points_x_array+ is an array holding the \(x\)--{}coordinates of the specified points. +% \begin{macrocode} +\array_new:N \g__hobby_points_x_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_points_y_array} +% \Verb+\g__hobby_points_y_array+ is an array holding the \(y\)--{}coordinates of the specified points. +% \begin{macrocode} +\array_new:N \g__hobby_points_y_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_actions_array} +% \Verb+\g__hobby_actions_array+ is an array holding the (encoded) action to be taken out on the segment of the path ending at that point. +% \begin{macrocode} +\array_new:N \g__hobby_actions_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_angles_array} +% \Verb+\g__hobby_angles_array+ is an array holding the angles of the lines between the points. +% Specifically, the angle indexed by \(k\) is the angle in radians of the line from \(z_k\) to \(z_{k+1}\). +% \begin{macrocode} +\array_new:N \g__hobby_angles_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_distances_array} +% \Verb+\g__hobby_distances_array+ is an array holding the distances between the points. +% Specifically, the distance indexed by \(k\), which we will write as \(d_k\), is the length of the line from \(z_k\) to \(z_{k+1}\). +% \begin{macrocode} +\array_new:N \g__hobby_distances_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_tension_out_array} +% \Verb+\g__hobby_tension_out_array+ is an array holding the tension for the path as it leaves each point. +% This is a parameter that controls how much the curve ``flexes'' as it leaves the point. +% In the following, this will be written \(\tau_k\). +% \begin{macrocode} +\array_new:N \g__hobby_tension_out_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_tension_in_array} +% \Verb+\g__hobby_tension_in_array+ is an array holding the tension for the path as it arrives at each point. +% This is a parameter that controls how much the curve ``flexes'' as it gets to the point. +% In the following, this will be written \(\overline{\tau}_k\). +% \begin{macrocode} +\array_new:N \g__hobby_tension_in_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_matrix_a_array} +% \Verb+\g__hobby_matrix_a_array+ is an array holding the subdiagonal of the linear system that has to be solved to find the angles of the control points. +% In the following, this will be denoted by \(A_i\). +% The first index is \(1\). +% \begin{macrocode} +\array_new:N \g__hobby_matrix_a_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_matrix_b_array} +% \Verb+\g__hobby_matrix_b_array+ is an array holding the diagonal of the linear system that has to be solved to find the angles of the control points. +% In the following, this will be denoted by \(B_i\). +% The first index is \(0\). +% \begin{macrocode} +\array_new:N \g__hobby_matrix_b_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_matrix_c_array} +% \Verb+\g__hobby_matrix_c_array+ is an array holding the superdiagonal of the linear system that has to be solved to find the angles of the control points. +% In the following, this will be denoted by \(C_i\). +% The first index is \(0\). +% \begin{macrocode} +\array_new:N \g__hobby_matrix_c_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_matrix_d_array} +% \Verb+\g__hobby_matrix_d_array+ is an array holding the target vector of the linear system that has to be solved to find the angles of the control points. +% In the following, this will be denoted by \(D_i\). +% The first index is \(1\). +% \begin{macrocode} +\array_new:N \g__hobby_matrix_d_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_vector_u_array} +% \Verb+\g__hobby_vector_u_array+ is an array holding the perturbation of the linear system for closed paths. +% The coefficient matrix for an \emph{open} path is tridiagonal and that means that Gaussian elimination runs faster than expected (\(O(n)\) instead of \(O(n^3)\)). +% The matrix for a closed path is not tridiagonal but is not far off. +% It can be solved by perturbing it to a tridiagonal matrix and then modifying the result. +% This array represents a utility vector in that perturbation. +% In the following, the vector will be denoted by \(u\). +% The first index is \(1\). +% \begin{macrocode} +\array_new:N \g__hobby_vector_u_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_excess_angle_array} +% \Verb+\g__hobby_excess_angle_array+ is an array that allows the user to say that the algorithm should add a multiple of \(2 \pi\) to the angle differences. +% This is because these angles are wrapped to the interval \((-\pi,\pi]\) but the wrapping might go wrong near the end points due to computation accuracy. +% The first index is \(1\). +% \begin{macrocode} +\array_new:N \g__hobby_excess_angle_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_psi_array} +% \Verb+\g__hobby_psi_array+ is an array holding the difference of the angles of the lines entering and exiting a point. +% That is, \(\psi_k\) is the angle between the lines joining \(z_k\) to \(z_{k-1}\) and \(z_{k+1}\). +% The first index is \(1\). +% \begin{macrocode} +\array_new:N \g__hobby_psi_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_theta_array} +% \Verb+\g__hobby_theta_array+ is an array holding the angles of the outgoing control points for the generated path. +% These are measured relative to the line joining the point to the next point on the path. +% The first index is \(0\). +% \begin{macrocode} +\array_new:N \g__hobby_theta_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_phi_array} +% \Verb+\g__hobby_phi_array+ is an array holding the angles of the incoming control points for the generated path. +% These are measured relative to the line joining the point to the previous point on the path. +% The first index is \(1\). +% \begin{macrocode} +\array_new:N \g__hobby_phi_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_sigma_array} +% \Verb+\g__hobby_sigma_array+ is an array holding the lengths of the outgoing control points for the generated path. +% The units are such that the length of the line to the next specified point is one unit. +% \begin{macrocode} +\array_new:N \g__hobby_sigma_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_rho_array} +% \Verb+\g__hobby_rho_array+ is an array holding the lengths of the incoming control points for the generated path. +% The units are such that the length of the line to the previous specified point is one unit. +% \begin{macrocode} +\array_new:N \g__hobby_rho_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_controla_array} +% \Verb+\g__hobby_controla_array+ is an array holding the coordinates of the first control points on the curves. +% The format is the same as for \Verb+\g__hobby_points_array+. +% \begin{macrocode} +\array_new:N \g__hobby_controla_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_controlb_array} +% \Verb+\g__hobby_controlb_array+ is an array holding the coordinates of the second control points on the curves. +% The format is the same as for \Verb+\g__hobby_points_array+. +% \begin{macrocode} +\array_new:N \g__hobby_controlb_array +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_matrix_v_fp} +% \Verb+\l_hobby_matrix_v_fp+ is a number which is used when doing the perturbation of the solution of the linear system for a closed curve. +% There is actually a vector, \(v\), that this corresponds to but that vector only has one component that needs computation. +% \begin{macrocode} +\fp_new:N \l_hobby_matrix_v_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_tempa_tl} +% \Verb+\l_hobby_tempa_tl+ is a temporary variable of type \Verb+tl+. +% \begin{macrocode} +\fp_new:N \l_hobby_tempa_tl +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_tempb_tl} +% \Verb+\l_hobby_tempb_tl+ is a temporary variable of type \Verb+tl+. +% \begin{macrocode} +\fp_new:N \l_hobby_tempb_tl +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_tempa_fp} +% \Verb+\l_hobby_tempa_fp+ is a temporary variable of type \Verb+fp+. +% \begin{macrocode} +\fp_new:N \l_hobby_tempa_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_tempb_fp} +% \Verb+\l_hobby_tempb_fp+ is a temporary variable of type \Verb+fp+. +% \begin{macrocode} +\fp_new:N \l_hobby_tempb_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_tempc_fp} +% \Verb+\l_hobby_tempc_fp+ is a temporary variable of type \Verb+fp+. +% \begin{macrocode} +\fp_new:N \l_hobby_tempc_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_tempd_fp} +% \Verb+\l_hobby_tempd_fp+ is a temporary variable of type \Verb+fp+. +% \begin{macrocode} +\fp_new:N \l_hobby_tempd_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\l_hobby_temps_fp} +% \Verb+\l_hobby_temps_fp+ is a temporary variable of type \Verb+fp+. +% \begin{macrocode} +\fp_new:N \l_hobby_temps_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_in_curl_fp} +% \Verb+\g__hobby_in_curl_fp+ is the ``curl'' at the end of an open path. +% This is used if the angle at the end is not specified. +% \begin{macrocode} +\fp_new:N \g__hobby_in_curl_fp +\fp_gset:Nn \g__hobby_in_curl_fp {1} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_out_curl_fp} +% \Verb+\g__hobby_out_curl_fp+ is the ``curl'' at the start of an open path. +% This is used if the angle at the start is not specified. +% \begin{macrocode} +\fp_new:N \g__hobby_out_curl_fp +\fp_gset:Nn \g__hobby_out_curl_fp {1} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_in_angle_fp} +% \Verb+\g__hobby_in_angle_fp+ is the angle at the end of an open path. +% If this is not specified, it will be computed automatically. +% It is set to \Verb+\c_inf_fp+ to allow easy detection of when it has been specified. +% \begin{macrocode} +\fp_new:N \g__hobby_in_angle_fp +\fp_gset_eq:NN \g__hobby_in_angle_fp \c_inf_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_out_angle_fp} +% \Verb+\g__hobby_out_angle_fp+ is the angle at the start of an open path. +% If this is not specified, it will be computed automatically. +% It is set to \Verb+\c_inf_fp+ to allow easy detection of when it has been specified. +% \begin{macrocode} +\fp_new:N \g__hobby_out_angle_fp +\fp_gset_eq:NN \g__hobby_out_angle_fp \c_inf_fp +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_npoints_int} +% \Verb+\g__hobby_npoints_int+ is one less than the number of points on the curve. +% As our list of points starts at \(0\), this is the index of the last point. +% In the algorithm for a closed curve, some points are repeated whereupon this is incremented so that it is always the index of the last point. +% \begin{macrocode} +\int_new:N \g__hobby_npoints_int +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\g__hobby_draw_int} +% \begin{macrocode} +\int_new:N \g__hobby_draw_int +% \end{macrocode} +% \end{macro} +% +% A ``point'' is a key-value list setting the x-value, the y-value, and the tensions at that point. +% Using keys makes it easier to pass points from the algorithm code to the calling code and vice versa without either knowing too much about the other. +% \begin{macrocode} +\keys_define:nn {hobby / read in all} { + point .code:n = { + \fp_set:Nn \l_hobby_tempa_fp {\clist_item:nn {#1} {1}} + \fp_set:Nn \l_hobby_tempb_fp {\clist_item:nn {#1} {2}} + }, + tension~out .fp_set:N = \l_hobby_tempc_fp, + tension~in .fp_set:N = \l_hobby_tempd_fp, + excess~angle .fp_set:N = \l_hobby_temps_fp, + break .tl_set:N = \l_tmpb_tl, + blank .tl_set:N = \l_tmpa_tl, + tension .meta:n = { tension~out=#1, tension~in=#1 }, + break .default:n = false, + blank .default:n = false, + invert~soft~blanks .choice:, + invert~soft~blanks / true .code:n = { + \int_gset:Nn \g__hobby_draw_int {0} + }, + invert~soft~blanks / false .code:n = { + \int_gset:Nn \g__hobby_draw_int {1} + }, + invert~soft~blanks .default:n = true, + tension~out .default:n = 1, + tension~in .default:n = 1, + excess~angle .default:n = 0, + in~angle .fp_gset:N = \g__hobby_in_angle_fp, + out~angle .fp_gset:N = \g__hobby_out_angle_fp, + in~curl .fp_gset:N = \g__hobby_in_curl_fp, + out~curl .fp_gset:N = \g__hobby_out_curl_fp, + closed .bool_gset:N = \g__hobby_closed_bool, + closed .default:n = true, + disjoint .bool_gset:N = \g__hobby_disjoint_bool, + disjoint .default:n = true, + break~default .code:n = { + \keys_define:nn { hobby / read in all } + { + break .default:n = #1 + } + }, + blank~default .code:n = { + \keys_define:nn { hobby / read in all } + { + blank .default:n = #1 + } + }, +} +% \end{macrocode} +% There are certain other parameters that can be set for a given curve. +% \begin{macrocode} +\keys_define:nn { hobby / read in params} { + in~angle .fp_gset:N = \g__hobby_in_angle_fp, + out~angle .fp_gset:N = \g__hobby_out_angle_fp, + in~curl .fp_gset:N = \g__hobby_in_curl_fp, + out~curl .fp_gset:N = \g__hobby_out_curl_fp, + closed .bool_gset:N = \g__hobby_closed_bool, + closed .default:n = true, + disjoint .bool_gset:N = \g__hobby_disjoint_bool, + disjoint .default:n = true, + break~default .code:n = { + \keys_define:nn { hobby / read in all } + { + break .default:n = #1 + } + }, + blank~default .code:n = { + \keys_define:nn { hobby / read in all } + { + blank .default:n = #1 + } + }, + invert~soft~blanks .choice:, + invert~soft~blanks / true .code:n = { + \int_gset:Nn \g__hobby_draw_int {0} + }, + invert~soft~blanks / false .code:n = { + \int_gset:Nn \g__hobby_draw_int {1} + }, + invert~soft~blanks .default:n = true, +} +% \end{macrocode} +% \begin{macro}{\hobby_distangle:n} +% Computes the distance and angle between successive points. +% The argument given is the index of the current point. +% Assumptions: the points are in \Verb+\g__hobby_points_x_array+ and \Verb+\g__hobby_points_y_array+ and the index of the last point is \Verb+\g__hobby_npoints_int+. +% \begin{macrocode} +\cs_set:Nn \hobby_distangle:n { + \fp_set:Nn \l_hobby_tempa_fp { + (\array_get:Nn \g__hobby_points_x_array {#1 + 1}) + - (\array_get:Nn \g__hobby_points_x_array {#1})} + + \fp_set:Nn \l_hobby_tempb_fp { + (\array_get:Nn \g__hobby_points_y_array {#1 + 1}) + - (\array_get:Nn \g__hobby_points_y_array {#1})} + + \fp_set:Nn \l_hobby_tempc_fp { atan ( \l_hobby_tempb_fp, \l_hobby_tempa_fp ) } + \fp_veclen:NVV \l_hobby_tempd_fp \l_hobby_tempa_fp \l_hobby_tempb_fp + + \array_gpush:Nx \g__hobby_angles_array {\fp_to_tl:N \l_hobby_tempc_fp} + \array_gpush:Nx \g__hobby_distances_array {\fp_to_tl:N \l_hobby_tempd_fp} + } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\fp_veclen:NVV} +% Computes the length of the vector specified by the latter two arguments, storing the answer in the first. +% \begin{macrocode} +\cs_new:Nn \fp_veclen:Nnn { + \fp_set:Nn #1 {((#2)^2 + (#3)^2)^.5} +} +\cs_generate_variant:Nn \fp_veclen:Nnn {NVV} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_ctrllen:Nnn} +% Computes the length of the control point vector from the two angles, storing the answer in the first argument given. +% \begin{macrocode} +\cs_new:Nn \hobby_ctrllen:Nnn { + \fp_set:Nn #1 {(2 - \g_hobby_parama_fp + * ( sin(#2) - \g_hobby_paramb_fp * sin(#3) ) + * ( sin(#3) - \g_hobby_paramb_fp * sin(#2) ) + * ( cos(#2) - cos(#3) ) ) + / ( 1 + (1 - \g_hobby_paramc_fp) * cos(#3) + \g_hobby_paramc_fp * cos(#2))} +} +\cs_generate_variant:Nn \hobby_ctrllen:Nnn {NVV} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_append_point_copy:n} +% This function adds a copy of the point (numbered by its argument) to the end of the list of points, copying all the relevant data (coordinates, tension, etc.). +% +% Originally from Bruno Le Foch on TeX-SX. +% \begin{macrocode} +\cs_new_protected:Npn \hobby_append_point_copy:n #1 + { + \hobby_append_point_copy_aux:Nn \g__hobby_points_array {#1} + \hobby_append_point_copy_aux:Nn \g__hobby_points_x_array {#1} + \hobby_append_point_copy_aux:Nn \g__hobby_points_y_array {#1} + \hobby_append_point_copy_aux:Nn \g__hobby_tension_in_array {#1} + \hobby_append_point_copy_aux:Nn \g__hobby_tension_out_array {#1} + \hobby_append_point_copy_aux:Nn \g__hobby_excess_angle_array {#1} + \hobby_append_point_copy_aux:Nn \g__hobby_actions_array {#1} + } +\cs_new_protected:Npn \hobby_append_point_copy_aux:Nn #1#2 + { \array_gpush:Nx #1 { \array_get:Nn #1 {#2} } } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_gen_path:} +% This is the curve generation function. +% We assume at the start that we have an array containing all the points that the curve must go through, and the various curve parameters have been initialised. +% So these must be set up by a wrapper function which then calls this one. +% The list of required information is: +% \begin{enumerate} +% \item \Verb+\g__hobby_points_x_array+ +% \item \Verb+\g__hobby_points_y_array+ +% \item \Verb+\g__hobby_tension_out_array+ +% \item \Verb+\g__hobby_tension_in_array+ +% \item \Verb+\g__hobby_excess_angle_array+ +% \item \Verb+\g__hobby_in_curl_fp+ +% \item \Verb+\g__hobby_out_curl_fp+ +% \item \Verb+\g__hobby_in_angle_fp+ +% \item \Verb+\g__hobby_out_angle_fp+ +% \item \Verb+\g__hobby_closed_bool+ +% \item \Verb+\g__hobby_actions_array+ +% \end{enumerate} +% +% \begin{macrocode} +\cs_new:Nn \hobby_gen_path: +{ +% \end{macrocode} +% For much of the time, we can pretend that a closed path is the same as an open path. +% To do this, we need to make the end node an internal node by repeating the \(z_1\) node as the \(z_{n+1}\)th node. +% We also check that the last (\(z_n\)) and first (\(z_0\)) nodes are the same, otherwise we repeat the \(z_0\) node as well. +% \begin{macrocode} +\bool_if:NT \g__hobby_closed_bool { +% \end{macrocode} +% Are the \(x\)-values of the first and last points different? +% \begin{macrocode} + \fp_compare:nTF {(\array_get:Nn \g__hobby_points_x_array {0}) + = + (\array_top:N \g__hobby_points_x_array)} + { +% \end{macrocode} +% No, so compare the \(y\)-values. +% Are the \(y\)-values of the first and last points different? +% \begin{macrocode} + \fp_compare:nF { + \array_get:Nn \g__hobby_points_y_array {0} + = + \array_top:N \g__hobby_points_y_array + } + { +% \end{macrocode} +% Yes, so we need to duplicate the first point, with all of its data. +% \begin{macrocode} + \hobby_append_point_copy:n {0} + } + } + { +% \end{macrocode} +% Yes, so we need to duplicate the first point, with all of its data. +% \begin{macrocode} + \hobby_append_point_copy:n {0} + } +% \end{macrocode} +% Now that we are sure that the first and last points are identical, we need to duplicate the first-but-one point (and all of its data). +% \begin{macrocode} + \hobby_append_point_copy:n {1} +} +% \end{macrocode} +% +% Set \Verb+\g__hobby_npoints_int+ to the number of points (minus one). +% \begin{macrocode} +\int_gset:Nn \g__hobby_npoints_int {\array_length:N \g__hobby_points_y_array} +% \end{macrocode} +% At this point, we need to decide what to do. +% This will depend on whether we have any intermediate points. +% \begin{macrocode} +\int_compare:nNnTF {\g__hobby_npoints_int} = {0} { +% \end{macrocode} +% Only one point, do nothing +% \begin{macrocode} +} +{ + \int_compare:nNnTF {\g__hobby_npoints_int} = {1} { +% \end{macrocode} +% Only two points, skip processing. +% Just need to set the incoming and outgoing angles +% \begin{macrocode} +\hobby_distangle:n {0} +\fp_compare:nF { \g__hobby_out_angle_fp == \c_inf_fp } +{ + \fp_set:Nn \l_hobby_tempa_fp { \g__hobby_out_angle_fp + - \array_get:Nn \g__hobby_angles_array {0}} +% \end{macrocode} +% We want to ensure that these angles lie in the range \((-\pi,\pi]\). +% So if the angle is bigger than \(\pi\), we subtract \(2 \pi\). +% (It shouldn't be that we can get bigger than \(3 \pi\) - check this) +% \begin{macrocode} + \fp_compare:nT {\l_hobby_tempa_fp > \c_pi_fp } + { + \fp_sub:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } +% \end{macrocode} +% Similarly, we check to see if the angle is less than \(-\pi\). +% \begin{macrocode} + \fp_compare:nT {\l_hobby_tempa_fp < -\c_pi_fp } + { + \fp_add:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + \array_gput:Nnx \g__hobby_theta_array {0} {\fp_to_tl:N \l_hobby_tempa_fp} + \fp_compare:nT { \g__hobby_in_angle_fp == \c_inf_fp } + { +%^^A \fp_mul:Nn \l_hobby_tempa_fp {-1} + \array_gput:Nnx \g__hobby_phi_array {1}{ \fp_to_tl:N \l_hobby_tempa_fp} + } + } +\fp_compare:nTF { \g__hobby_in_angle_fp == \c_inf_fp } +{ + \fp_compare:nT { \g__hobby_out_angle_fp == \c_inf_fp } + { + \array_gput:Nnx \g__hobby_phi_array {1} {0} + \array_gput:Nnx \g__hobby_theta_array {0} {0} + } +} +{ + \fp_set:Nn \l_hobby_tempa_fp { - \g__hobby_in_angle_fp + \c_pi_fp ++ (\array_get:Nn \g__hobby_angles_array {0})} + \fp_compare:nT {\l_hobby_tempa_fp > \c_pi_fp } + { + \fp_sub:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + \fp_compare:nT {\l_hobby_tempa_fp < -\c_pi_fp } + { + \fp_add:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + + \array_gput:Nnx \g__hobby_phi_array {1} + {\fp_to_tl:N \l_hobby_tempa_fp} + \fp_compare:nT { \g__hobby_out_angle_fp == \c_inf_fp } + { +%^^A \fp_mul:Nn \l_hobby_tempa_fp {-1} + \array_gput:Nnx \g__hobby_theta_array {0}{ \fp_to_tl:N \l_hobby_tempa_fp} + } +} + + } + { +% \end{macrocode} +% Got enough points, go on with processing +% \begin{macrocode} + \hobby_compute_path: + } + \hobby_build_path: +} +} +% \end{macrocode} +% \end{macro} +% +% +% \begin{macro}{\hobby_compute_path:} +% This is the path builder where we have enough points to run the algorithm. +% \begin{macrocode} +\cs_new:Nn \hobby_compute_path: +{ +% \end{macrocode} +% Our first step is to go through the list of points and compute the distances and angles between successive points. +% Thus \(d_i\) is the distance from \(z_i\) to \(z_{i+1}\) and the angle is the angle of the line from \(z_i\) to \(z_{i+1}\). +% \begin{macrocode} +\int_step_function:nnnN {0} {1} {\g__hobby_npoints_int - 1} \hobby_distangle:n +% \end{macrocode} +% +% For the majority of the code, we're only really interested in the differences of the angles. +% So for each internal point we compute the differences in the angles. +% \begin{macrocode} + \int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 1} { + \fp_set:Nx \l_hobby_tempa_fp { + \array_get:Nn \g__hobby_angles_array {##1} + - \array_get:Nn \g__hobby_angles_array {##1 - 1}} +% \end{macrocode} +% We want to ensure that these angles lie in the range \((-\pi,\pi]\). +% So if the angle is bigger than \(\pi\), we subtract \(2 \pi\). +% (It shouldn't be that we can get bigger than \(3 \pi\) - check this.) +% \begin{macrocode} + \fp_compare:nTF {\l_hobby_tempa_fp > \c_pi_fp } + { + \fp_sub:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + {} +% \end{macrocode} +% Similarly, we check to see if the angle is less than \(-\pi\). +% \begin{macrocode} + \fp_compare:nTF {\l_hobby_tempa_fp <= -\c_pi_fp } + { + \fp_add:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + {} +% \end{macrocode} +% The wrapping routine might not get it right at the edges so we add in the override. +% \begin{macrocode} +\array_get:NnNTF \g__hobby_excess_angle_array {##1} \l_tmpa_tl { + \fp_add:Nn \l_hobby_tempa_fp {2 * \c_pi_fp * \l_tmpa_tl} + }{} +% \end{macrocode} +% \begin{macrocode} + \array_gput:Nnx \g__hobby_psi_array {##1}{\fp_to_tl:N \l_hobby_tempa_fp} + } +% \end{macrocode} +% +% Next, we generate the matrix. +% We start with the subdiagonal. +% This is indexed from \(1\) to \(n-1\). +% \begin{macrocode} + \int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 1} { + \array_gput:Nnx \g__hobby_matrix_a_array {##1} {\fp_to_tl:n { + \array_get:Nn \g__hobby_tension_in_array {##1}^2 + * \array_get:Nn \g__hobby_distances_array {##1} + * \array_get:Nn \g__hobby_tension_in_array {##1 + 1} + }} +} +% \end{macrocode} +% +% Next, we attack main diagonal. +% We might need to adjust the first and last terms, but we'll do that in a minute. +% \begin{macrocode} + \int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 1} { + + \array_gput:Nnx \g__hobby_matrix_b_array {##1} {\fp_to_tl:n +{(3 * (\array_get:Nn \g__hobby_tension_in_array {##1 + 1}) - 1) * + (\array_get:Nn \g__hobby_tension_out_array {##1})^2 * +(\array_get:Nn \g__hobby_tension_out_array {##1 - 1}) +* ( \array_get:Nn \g__hobby_distances_array {##1 - 1}) ++ +(3 * (\array_get:Nn \g__hobby_tension_out_array {##1 - 1}) - 1) +* (\array_get:Nn \g__hobby_tension_in_array {##1})^2 +* (\array_get:Nn \g__hobby_tension_in_array {##1 + 1}) +* (\array_get:Nn \g__hobby_distances_array {##1})} +} +} +% \end{macrocode} +% +% Next, the superdiagonal. +% \begin{macrocode} + \int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 2} { + + \array_gput:Nnx \g__hobby_matrix_c_array {##1} {\fp_to_tl:n +{(\array_get:Nn \g__hobby_tension_in_array {##1})^2 +* (\array_get:Nn \g__hobby_tension_in_array {##1 - 1}) +* (\array_get:Nn \g__hobby_distances_array {##1 - 1}) +}} + +} +% \end{macrocode} +% +% Lastly (before the adjustments), the target vector. +% \begin{macrocode} + \int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 2} { + + \array_gput:Nnx \g__hobby_matrix_d_array {##1} {\fp_to_tl:n +{ +- (\array_get:Nn \g__hobby_psi_array {##1 + 1}) +* (\array_get:Nn \g__hobby_tension_out_array {##1})^2 +* (\array_get:Nn \g__hobby_tension_out_array {##1 - 1}) +* (\array_get:Nn \g__hobby_distances_array {##1 - 1}) +- (3 * (\array_get:Nn \g__hobby_tension_out_array {##1 - 1}) - 1) +* (\array_get:Nn \g__hobby_psi_array {##1}) +* (\array_get:Nn \g__hobby_tension_in_array {##1})^2 +* (\array_get:Nn \g__hobby_tension_in_array {##1 + 1}) +* (\array_get:Nn \g__hobby_distances_array {##1}) +} +} +} +% \end{macrocode} +% +% Next, there are some adjustments at the ends. +% These differ depending on whether the path is open or closed. +% \begin{macrocode} +\bool_if:NTF \g__hobby_closed_bool { +% \end{macrocode} +% Closed path +% \begin{macrocode} +\array_gput:Nnx \g__hobby_matrix_c_array {0} {\fp_to_tl:n { +- (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int - 2}) +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 2}) +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 1})^2 +}} + +\array_gput:Nnn \g__hobby_matrix_b_array {0} {1} +\array_gput:Nnn \g__hobby_matrix_d_array {0} {0} + +\array_gput:Nnx \g__hobby_matrix_b_array {\g__hobby_npoints_int - 1} {\fp_to_tl:n { +(\array_get:Nn \g__hobby_matrix_b_array {\g__hobby_npoints_int - 1}) ++ 1 +}} + + \array_gput:Nnx \g__hobby_matrix_d_array {\g__hobby_npoints_int - 1} {\fp_to_tl:n { +- (\array_get:Nn \g__hobby_psi_array {1}) +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int -1})^2 +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int -2}) +* (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int - 2}) +- (3 * (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 2}) - 1) +* (\array_get:Nn \g__hobby_psi_array {\g__hobby_npoints_int - 1}) +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int - 1})^2 +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int}) +* (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int -1}) +} +} +% \end{macrocode} +% We also need to populate the \(u\)-vector +% \begin{macrocode} + \array_gput:Nnn \g__hobby_vector_u_array {0} {1} +\array_gput:Nnn \g__hobby_vector_u_array {\g__hobby_npoints_int - 1} {1} + \int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 2} { + \array_gput:Nnn \g__hobby_vector_u_array {##1} {0} + } +% \end{macrocode} +% And define the significant entry in the \(v\)-vector. +% \begin{macrocode} +\fp_set:Nn \l_hobby_matrix_v_fp { +(\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int -1})^2 +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int -2}) +* (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int -2}) +} +} +{ +% \end{macrocode} +% Open path. +% First, we test to see if \(\theta_0\) has been specified. +% \begin{macrocode} +\fp_compare:nTF { \g__hobby_out_angle_fp == \c_inf_fp } +{ + \array_gput:Nnx \g__hobby_matrix_b_array {0} {\fp_to_tl:n { + (\array_get:Nn \g__hobby_tension_in_array {1})^3 +* \g__hobby_in_curl_fp ++ +(3 * (\array_get:Nn \g__hobby_tension_in_array {1}) - 1) +* (\array_get:Nn \g__hobby_tension_out_array {0})^3 +}} + + \array_gput:Nnx \g__hobby_matrix_c_array {0} {\fp_to_tl:n { + (\array_get:Nn \g__hobby_tension_out_array {0})^3 ++ +(3 * (\array_get:Nn \g__hobby_tension_out_array {0}) - 1) +* (\array_get:Nn \g__hobby_tension_in_array {1})^3 +* \g__hobby_in_curl_fp +}} + + \array_gput:Nnx \g__hobby_matrix_d_array {0} {\fp_to_tl:n { +-( (\array_get:Nn \g__hobby_tension_out_array {0})^3 ++ +(3 * (\array_get:Nn \g__hobby_tension_out_array {0}) - 1) +* (\array_get:Nn \g__hobby_tension_in_array {1})^3 +* \g__hobby_in_curl_fp) +* (\array_get:Nn \g__hobby_psi_array {1}) +}} + +} +{ + \array_gput:Nnn \g__hobby_matrix_b_array {0} {1} + \array_gput:Nnn \g__hobby_matrix_c_array {0} {0} + \fp_set:Nn \l_hobby_tempa_fp { \g__hobby_out_angle_fp + - \array_get:Nn \g__hobby_angles_array {0}} +% \end{macrocode} +% We want to ensure that these angles lie in the range \((-\pi,\pi]\). +% So if the angle is bigger than \(\pi\), we subtract \(2 \pi\). +% (It shouldn't be that we can get bigger than \(3 \pi\) - check this) +% \begin{macrocode} + \fp_compare:nT {\l_hobby_tempa_fp > \c_pi_fp } + { + \fp_sub:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } +% \end{macrocode} +% Similarly, we check to see if the angle is less than \(-\pi\). +% \begin{macrocode} + \fp_compare:nT {\l_hobby_tempa_fp < -\c_pi_fp } + { + \fp_add:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + \array_gput:Nnx \g__hobby_matrix_d_array {0} {\fp_to_tl:N \l_hobby_tempa_fp} +} +% \end{macrocode} +% +% Next, if \(\phi_n\) has been given. +% \begin{macrocode} +\fp_compare:nTF { \g__hobby_in_angle_fp == \c_inf_fp } +{ + + \array_gput:Nnx \g__hobby_matrix_b_array {\g__hobby_npoints_int - 1} {\fp_to_tl:n { +\array_get:Nn \g__hobby_matrix_b_array {\g__hobby_npoints_int - 1} +- (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 1})^2 +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 2}) +* (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int - 2}) +* +((3 * (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int} ) - 1) +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 1})^3 \l_tmpa_tl +* \g__hobby_out_curl_fp ++ +(\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int })^3) +/ +((3 * (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int -2}) - 1) +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int})^3 ++ +( \array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 1})^3 +* \g__hobby_out_curl_fp) +}} + + \array_gput:Nnx \g__hobby_matrix_d_array {\g__hobby_npoints_int - 1} {\fp_to_tl:n { +- (3 * (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 2}) - 1) +* (\array_get:Nn \g__hobby_psi_array {\g__hobby_npoints_int - 1}) +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int - 1})^2 +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int}) +* (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int - 1}) +}} + +} +{ + \fp_set:Nn \l_hobby_tempa_fp { - \g__hobby_in_angle_fp + \c_pi_fp ++ (\array_get:Nn \g__hobby_angles_array {\g__hobby_npoints_int - 1})} + \fp_compare:nT {\l_hobby_tempa_fp > \c_pi_fp } + { + \fp_sub:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + \fp_compare:nT {\l_hobby_tempa_fp < -\c_pi_fp } + { + \fp_add:Nn \l_hobby_tempa_fp {2 * \c_pi_fp} + } + + \array_gput:Nnx \g__hobby_phi_array {\g__hobby_npoints_int} + {\fp_to_tl:N \l_hobby_tempa_fp} + + \array_gput:Nnx \g__hobby_matrix_d_array {\g__hobby_npoints_int - 1} {\fp_to_tl:n { + \l_hobby_tempa_fp + * (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 1})^2 +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 2}) +* (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int - 2}) +- +(3 * ( \array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 2}) - 1) +* (\array_get:Nn \g__hobby_psi_array {\g__hobby_npoints_int - 1}) +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int - 1})^2 +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int}) +* (\array_get:Nn \g__hobby_distances_array {\g__hobby_npoints_int - 1}) }} +} +% \end{macrocode} +% End of adjustments for open paths. +% \begin{macrocode} +} +% \end{macrocode} +% +% Now we have the tridiagonal matrix in place, we implement the solution. +% We start with the forward eliminations. +% \begin{macrocode} +\int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 1} { + + \array_gput:Nnx \g__hobby_matrix_b_array {##1} {\fp_to_tl:n { + (\array_get:Nn \g__hobby_matrix_b_array {##1 - 1}) +* (\array_get:Nn \g__hobby_matrix_b_array {##1}) +- +(\array_get:Nn \g__hobby_matrix_c_array {##1 - 1}) +* (\array_get:Nn \g__hobby_matrix_a_array {##1}) +}} +% \end{macrocode} +% The last time, we don't touch the \(C\)-vector. +% \begin{macrocode} + \int_compare:nT {##1 < \g__hobby_npoints_int - 1} { + + \array_gput:Nnx \g__hobby_matrix_c_array {##1} {\fp_to_tl:n { +(\array_get:Nn \g__hobby_matrix_b_array {##1 - 1}) + * (\array_get:Nn \g__hobby_matrix_c_array {##1}) +}} + } + + \array_gput:Nnx \g__hobby_matrix_d_array {##1} {\fp_to_tl:n { +(\array_get:Nn \g__hobby_matrix_b_array {##1 - 1}) + * (\array_get:Nn \g__hobby_matrix_d_array {##1}) +- + (\array_get:Nn \g__hobby_matrix_d_array {##1 - 1}) + * (\array_get:Nn \g__hobby_matrix_a_array {##1}) +}} +% \end{macrocode} +% On a closed path, we also want to know \(M^{-1} u\) so need to do the elimination steps on \(u\) as well. +% \begin{macrocode} + \bool_if:NT \g__hobby_closed_bool { + \array_gput:Nnx \g__hobby_vector_u_array {##1} {\fp_to_tl:n { +(\array_get:Nn \g__hobby_matrix_b_array {##1 - 1}) +* (\array_get:Nn \g__hobby_vector_u_array {##1}) +- +(\array_get:Nn \g__hobby_vector_u_array {##1 - 1}) +* (\array_get:Nn \g__hobby_matrix_a_array {##1}) +}} +} +} +% \end{macrocode} +% Now we start the back substitution. +% The first step is slightly different to the general step. +% \begin{macrocode} + \array_gput:Nnx \g__hobby_theta_array {\g__hobby_npoints_int - 1} {\fp_to_tl:n { +(\array_get:Nn \g__hobby_matrix_d_array {\g__hobby_npoints_int - 1}) +/ (\array_get:Nn \g__hobby_matrix_b_array {\g__hobby_npoints_int - 1}) +}} +% \end{macrocode} +% For a closed path, we need to work with \(u\) as well. +% \begin{macrocode} +\bool_if:NT \g__hobby_closed_bool { + \array_gput:Nnx \g__hobby_vector_u_array {\g__hobby_npoints_int - 1} {\fp_to_tl:n { + (\array_get:Nn \g__hobby_vector_u_array {\g__hobby_npoints_int - 1}) +/ (\array_get:Nn \g__hobby_matrix_b_array {\g__hobby_npoints_int - 1}) +}} +} +% \end{macrocode} +% Now we iterate over the vectors, doing the remaining back substitutions. +% \begin{macrocode} +\int_step_inline:nnnn {\g__hobby_npoints_int - 2} {-1} {0} { + + \array_gput:Nnx \g__hobby_theta_array {##1} {\fp_to_tl:n { +( (\array_get:Nn \g__hobby_matrix_d_array {##1}) + - (\array_get:Nn \g__hobby_theta_array {##1 + 1}) + * (\array_get:Nn \g__hobby_matrix_c_array {##1}) +) / (\array_get:Nn \g__hobby_matrix_b_array {##1}) +}} +} +\bool_if:NT \g__hobby_closed_bool { +% \end{macrocode} +% On a closed path, we also need to work out \(M^{-1} u\). +% \begin{macrocode} +\int_step_inline:nnnn {\g__hobby_npoints_int - 2} {-1} {0} { + \array_gput:Nnx \g__hobby_vector_u_array {##1} {\fp_to_tl:n +{ + ((\array_get:Nn \g__hobby_vector_u_array {##1}) + - (\array_get:Nn \g__hobby_vector_u_array {##1 + 1}) + * (\array_get:Nn \g__hobby_matrix_c_array {##1}) + ) / (\array_get:Nn \g__hobby_matrix_b_array {##1}) +}} +} +% \end{macrocode} +% Then we compute \(v^\top M^{-1}u\) and \(v^\top M^{-1} \theta\). +% As \(v\) has a particularly simple form, these inner products are easy to compute. +% \begin{macrocode} + +\fp_set:Nn \l_hobby_tempb_fp { +((\array_get:Nn \g__hobby_theta_array {1}) +* \l_hobby_matrix_v_fp +- (\array_get:Nn \g__hobby_theta_array {\g__hobby_npoints_int - 1}) +) / ( +(\array_get:Nn \g__hobby_vector_u_array {1}) +* \l_hobby_matrix_v_fp +- (\array_get:Nn \g__hobby_vector_u_array {\g__hobby_npoints_int - 1}) ++ 1 +)} + +\int_step_inline:nnnn {0} {1} {\g__hobby_npoints_int - 1} { + + \array_gput:Nnx \g__hobby_theta_array {##1} {\fp_to_tl:n { + (\array_get:Nn \g__hobby_theta_array {##1}) + - (\array_get:Nn \g__hobby_vector_u_array {##1}) + * \l_hobby_tempb_fp +}} +} +} +% \end{macrocode} +% +% Now that we have computed the \(\theta_i\)s, we can quickly compute the \(\phi_i\)s. +% +% \begin{macrocode} +\int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int - 1} { + + \array_gput:Nnx \g__hobby_phi_array {##1} {\fp_to_tl:n { + - (\array_get:Nn \g__hobby_psi_array {##1}) + - (\array_get:Nn \g__hobby_theta_array {##1}) + }} + } +% \end{macrocode} +% +% If the path is open, this works for all except \(\phi_n\). +% If the path is closed, we can drop our added point. +% Cheaply, of course. +% \begin{macrocode} +\bool_if:NTF \g__hobby_closed_bool { + \int_gdecr:N \g__hobby_npoints_int +}{ +% \end{macrocode} +% If \(\phi_n\) was not given, we compute it from \(\theta_{n-1}\). +% \begin{macrocode} +\fp_compare:nT { \g__hobby_in_angle_fp == \c_inf_fp } +{ + \array_gput:Nnx \g__hobby_phi_array {\g__hobby_npoints_int} {\fp_to_tl:n { +((3 * (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int}) - 1) +* (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 1})^3 +* \g__hobby_out_curl_fp ++ +(\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int })^3) +/ +((3 * (\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int -2}) - 1) +* (\array_get:Nn \g__hobby_tension_in_array {\g__hobby_npoints_int})^3 \l_tmpa_tl ++ +(\array_get:Nn \g__hobby_tension_out_array {\g__hobby_npoints_int - 1})^3 +* \g__hobby_out_curl_fp) +* +(\array_get:Nn \g__hobby_theta_array {\g__hobby_npoints_int -1}) +}} +} +} +} +% \end{macrocode} +% \end{macro} +% +% +% \begin{macro}{\hobby_build_path:} +% Once we've computed the angles, we build the actual path. +% \begin{macrocode} +\cs_new:Nn \hobby_build_path: +{ +% \end{macrocode} +% Next task is to compute the \(\rho_i\) and \(\sigma_i\). +% \begin{macrocode} +\int_step_inline:nnnn {0} {1} {\g__hobby_npoints_int - 1} { + + \fp_set:Nn \l_hobby_tempa_fp {\array_get:Nn \g__hobby_theta_array {##1}} + + \fp_set:Nn \l_hobby_tempb_fp {\array_get:Nn \g__hobby_phi_array {##1 + 1}} + + \hobby_ctrllen:NVV \l_hobby_temps_fp \l_hobby_tempa_fp \l_hobby_tempb_fp + + \array_gput:Nnx \g__hobby_sigma_array {##1 + 1} {\fp_to_tl:N \l_hobby_temps_fp} + + \hobby_ctrllen:NVV \l_hobby_temps_fp \l_hobby_tempb_fp \l_hobby_tempa_fp + + \array_gput:Nnx \g__hobby_rho_array {##1} {\fp_to_tl:N \l_hobby_temps_fp} + + } +% \end{macrocode} +% Lastly, we generate the coordinates of the control points. +% \begin{macrocode} +\int_step_inline:nnnn {0} {1} {\g__hobby_npoints_int - 1} { +\array_gput:Nnx \g__hobby_controla_array {##1 + 1} {\fp_eval:n { +(\array_get:Nn \g__hobby_points_x_array {##1}) ++ + (\array_get:Nn \g__hobby_distances_array {##1}) * + (\array_get:Nn \g__hobby_rho_array {##1}) * +cos ( (\array_get:Nn \g__hobby_angles_array {##1}) ++ + (\array_get:Nn \g__hobby_theta_array {##1})) +/3 +}, \fp_eval:n { +( \array_get:Nn \g__hobby_points_y_array {##1}) + + (\array_get:Nn \g__hobby_distances_array {##1}) * + (\array_get:Nn \g__hobby_rho_array {##1}) * +sin ( (\array_get:Nn \g__hobby_angles_array {##1}) ++ + (\array_get:Nn \g__hobby_theta_array {##1})) +/3 +} +} +} +\int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int} { + \array_gput:Nnx \g__hobby_controlb_array {##1} { + \fp_eval:n {\array_get:Nn \g__hobby_points_x_array {##1} +- (\array_get:Nn \g__hobby_distances_array {##1 - 1}) +* (\array_get:Nn \g__hobby_sigma_array {##1}) +* cos((\array_get:Nn \g__hobby_angles_array {##1 - 1}) +- (\array_get:Nn \g__hobby_phi_array {##1}))/3 +}, \fp_eval:n { + (\array_get:Nn \g__hobby_points_y_array {##1}) +- (\array_get:Nn \g__hobby_distances_array {##1 - 1}) +* (\array_get:Nn \g__hobby_sigma_array {##1}) +* sin((\array_get:Nn \g__hobby_angles_array {##1 - 1}) +- (\array_get:Nn \g__hobby_phi_array {##1}))/3 +} } + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbyinit} +% Initialise the settings for Hobby's algorithm +% \begin{macrocode} +\NewDocumentCommand \hobbyinit {m m m} { + \hobby_set_cmds:NNN #1#2#3 + \hobby_clear_path: +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbyaddpoint} +% This adds a point, possibly with tensions, to the current stack. +% \begin{macrocode} +\NewDocumentCommand \hobbyaddpoint { m } { + \keys_set:nn { hobby/read in all } + { + tension~out, + tension~in, + excess~angle, + blank, + break, + #1 + } + \tl_if_eq:VnTF \l_tmpa_tl {true} + {\tl_set:Nn \l_tmpa_tl {2}} + { + \tl_if_eq:VnTF \l_tmpa_tl {soft} + {\tl_set:Nn \l_tmpa_tl {0}} + {\tl_set:Nn \l_tmpa_tl {1}} + } + \tl_if_eq:VnTF \l_tmpb_tl {true} + {\tl_put_right:Nn \l_tmpa_tl {1}} + {\tl_put_right:Nn \l_tmpa_tl {0}} + \tl_set:Nx \l_hobby_tempa_tl {\fp_use:N \l_hobby_tempa_fp} + \tl_set:Nx \l_hobby_tempb_tl {\fp_use:N \l_hobby_tempb_fp} + \hobby_add_point:VVVVVV \l_hobby_tempa_tl \l_hobby_tempb_tl \l_hobby_tempc_fp \l_hobby_tempd_fp \l_hobby_temps_fp \l_tmpa_tl +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_add_point:n} +% \begin{macrocode} +\cs_new_nopar:Npn \hobby_add_point:nnnnnn #1#2#3#4#5#6 +{ + \array_gpush:Nn \g__hobby_actions_array { #6 } + \array_gpush:Nn \g__hobby_tension_out_array { #3 } + \array_gpush:Nn \g__hobby_tension_in_array { #4 } + \array_gpush:Nn \g__hobby_excess_angle_array { #5 } + \array_gpush:Nn \g__hobby_points_array { #1, #2 } + \array_gpush:Nn \g__hobby_points_x_array { #1 } + \array_gpush:Nn \g__hobby_points_y_array { #2 } +} +\cs_generate_variant:Nn \hobby_add_point:nnnnnn {VVVVVV} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbysetparams} +% This sets the parameters for the curve. +% \begin{macrocode} +\NewDocumentCommand \hobbysetparams { m } { + \keys_set:nn { hobby / read in params } + { + #1 + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_set_cmds:NNN} +% The path-generation code doesn't know what to actually do with the path so the initialisation code will set some macros to do that. +% This is an auxiliary command that sets these macros. +% \begin{macrocode} +\cs_new:Npn \hobby_moveto:nnn #1#2#3 {} +\cs_new:Npn \hobby_curveto:nnn #1#2#3 {} +\cs_new:Npn \hobby_close:n #1 {} +\cs_generate_variant:Nn \hobby_moveto:nnn {VVV,nnV} +\cs_generate_variant:Nn \hobby_curveto:nnn {VVV} +\cs_generate_variant:Nn \hobby_close:n {V} +\cs_new:Nn \hobby_set_cmds:NNN { + \cs_gset_eq:NN \hobby_moveto:nnn #1 + \cs_gset_eq:NN \hobby_curveto:nnn #2 + \cs_gset_eq:NN \hobby_close:n #3 +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbygenpath} +% This is the user (well, sort of) command that generates the curve. +% \begin{macrocode} +\NewDocumentCommand \hobbygenpath { } { + \array_if_empty:NF \g__hobby_points_array { + \hobby_gen_path: + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbygenifnecpath} +% If the named path doesn't exist, it is generated and named. +% If it does exist, we restore it. +% Either way, we save it to the aux file. +% \begin{macrocode} +\NewDocumentCommand \hobbygenifnecpath { m } { + \tl_if_exist:cTF {g_hobby_#1_path} + { + \tl_use:c {g_hobby_#1_path} + } + { + \hobby_gen_path: + } + \hobby_save_path:n {#1} + \hobby_save_path_to_aux:x {#1} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbygenifnecusepath} +% If the named path doesn't exist, it is generated and named. +% If it does exist, we restore it. +% Either way, we save it to the aux file. +% \begin{macrocode} +\NewDocumentCommand \hobbygenuseifnecpath { m } { + \tl_if_exist:cTF {g_hobby_#1_path} + { + \tl_use:c {g_hobby_#1_path} + } + { + \hobby_gen_path: + } + \hobby_save_path:n {#1} + \hobby_save_path_to_aux:x {#1} + \hobby_use_path: +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbyusepath} +% This is the user (well, sort of) command that uses the last generated curve. +% \begin{macrocode} +\NewDocumentCommand \hobbyusepath { m } { + \hobbysetparams{#1} + \hobby_use_path: +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbysavepath} +% This is the user (well, sort of) command that uses the last generated curve. +% \begin{macrocode} +\NewDocumentCommand \hobbysavepath { m } { + \hobby_save_path:n {#1} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbyrestorepath} +% This is the user (well, sort of) command that uses the last generated curve. +% \begin{macrocode} +\NewDocumentCommand \hobbyrestorepath { m } { + \tl_if_exist:cT {g_hobby_#1_path} { + \tl_use:c {g_hobby_#1_path} + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbyshowpath} +% This is the user (well, sort of) command that uses the last generated curve. +% \begin{macrocode} +\NewDocumentCommand \hobbyshowpath { m } { + \tl_if_exist:cT {g_hobby_#1_path} { + \tl_show:c {g_hobby_#1_path} + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbygenusepath} +% This is the user (well, sort of) command that generates a curve and uses it. +% \begin{macrocode} +\NewDocumentCommand \hobbygenusepath { } { + \array_if_empty:NF \g__hobby_points_array { + \hobby_gen_path: + \hobby_use_path: + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobbyclearpath} +% This is the user (well, sort of) command that generates a curve and uses it. +% \begin{macrocode} +\NewDocumentCommand \hobbyclearpath { } { + \hobby_clear_path: +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_use_path:} +% This is the command that uses the curve. +% As the curve data is stored globally, the same data can be reused by calling this function more than once without calling the generating function. +% \begin{macrocode} +\tl_new:N \l_tmpc_tl +\tl_new:N \l_tmpd_tl +\cs_new:Nn \hobby_use_path: { + \bool_if:NT \g__hobby_disjoint_bool { + \array_get:NnN \g__hobby_points_array {0} \l_tmpa_tl + \hobby_moveto:nnV {} {} \l_tmpa_tl + } + \int_step_inline:nnnn {1} {1} {\g__hobby_npoints_int} { + \array_get:NnN \g__hobby_controla_array {##1} \l_tmpa_tl + \array_get:NnN \g__hobby_controlb_array {##1} \l_tmpb_tl + \array_get:NnN \g__hobby_points_array {##1} \l_tmpc_tl + \array_get:NnN \g__hobby_actions_array {##1} \l_tmpd_tl + \int_compare:nNnTF {\tl_item:Nn \l_tmpd_tl {1}} = {\g__hobby_draw_int} { + \hobby_curveto:VVV \l_tmpa_tl \l_tmpb_tl \l_tmpc_tl + }{ + \bool_gset_false:N \g__hobby_closed_bool + \hobby_moveto:VVV \l_tmpa_tl \l_tmpb_tl \l_tmpc_tl + } + \tl_if_eq:xnTF {\tl_item:Nn \l_tmpd_tl {2}} {1} { + \bool_gset_false:N \g__hobby_closed_bool + \hobby_moveto:VVV \l_tmpa_tl \l_tmpb_tl \l_tmpc_tl + }{} + } + \bool_if:NT \g__hobby_closed_bool { + \array_get:NnN \g__hobby_points_array {0} \l_tmpa_tl + \hobby_close:V \l_tmpa_tl + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_save_path:n} +% This command saves all the data needed to reinvoke the curve in a global token list that can be used to restore it afterwards. +% \begin{macrocode} +\cs_new:Nn \hobby_save_path:n { + \tl_clear:N \l_tmpa_tl + \tl_put_right:Nn \l_tmpa_tl {\int_gset:Nn \g__hobby_npoints_int} + \tl_put_right:Nx \l_tmpa_tl {{\int_use:N \g__hobby_npoints_int}} + \bool_if:NTF \g__hobby_disjoint_bool { + \tl_put_right:Nn \l_tmpa_tl {\bool_gset_true:N} + }{ + \tl_put_right:Nn \l_tmpa_tl {\bool_gset_false:N} + } + \tl_put_right:Nn \l_tmpa_tl {\g__hobby_disjoint_bool} + \bool_if:NTF \g__hobby_closed_bool { + \tl_put_right:Nn \l_tmpa_tl {\bool_gset_true:N} + }{ + \tl_put_right:Nn \l_tmpa_tl {\bool_gset_false:N} + } + \tl_put_right:Nn \l_tmpa_tl {\g__hobby_closed_bool} + \tl_put_right:Nn \l_tmpa_tl {\array_gclear:N \g__hobby_points_array} + \array_map_inline:Nn \g__hobby_points_array { + \tl_put_right:Nn \l_tmpa_tl { + \array_gput:Nnn \g__hobby_points_array {##1} {##2} + } + } + \tl_put_right:Nn \l_tmpa_tl {\array_gclear:N \g__hobby_actions_array} + \array_map_inline:Nn \g__hobby_actions_array { + \tl_put_right:Nn \l_tmpa_tl { + \array_gput:Nnn \g__hobby_actions_array {##1} {##2} + } + } + \tl_put_right:Nn \l_tmpa_tl {\array_gclear:N \g__hobby_controla_array} + \array_map_inline:Nn \g__hobby_controla_array { + \tl_put_right:Nn \l_tmpa_tl { + \array_gput:Nnn \g__hobby_controla_array {##1} {##2} + } + } + \tl_put_right:Nn \l_tmpa_tl {\array_gclear:N \g__hobby_controlb_array} + \array_map_inline:Nn \g__hobby_controlb_array { + \tl_put_right:Nn \l_tmpa_tl { + \array_gput:Nnn \g__hobby_controlb_array {##1} {##2} + } + } + \tl_gclear_new:c {g_hobby_#1_path} + \tl_gset_eq:cN {g_hobby_#1_path} \l_tmpa_tl +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_save_path_to_aux:n} +% \begin{macrocode} +\int_set:Nn \l_tmpa_int {\char_value_catcode:n {`@}} +\char_set_catcode_letter:N @ +\cs_new:Npn \hobby_save_path_to_aux:n #1 { + \bool_if:nT { + \tl_if_exist_p:c {g_hobby_#1_path} + && + ! \tl_if_exist_p:c {g_hobby_#1_path_saved} + && + \l_hobby_save_aux_bool + } + { + \tl_clear:N \l_tmpa_tl + \tl_put_right:Nn \l_tmpa_tl { + \ExplSyntaxOn + \tl_gclear_new:c {g_hobby_#1_path} + \tl_gput_right:cn {g_hobby_#1_path} + } + \tl_put_right:Nx \l_tmpa_tl { + {\tl_to_str:c {g_hobby_#1_path}} + } + \tl_put_right:Nn \l_tmpa_tl { + \ExplSyntaxOff + } + \protected@write\@auxout{}{ + \tl_to_str:N \l_tmpa_tl + } + \tl_new:c {g_hobby_#1_path_saved} + } +} +\char_set_catcode:nn {`@} {\l_tmpa_int} +\cs_generate_variant:Nn \hobby_save_path_to_aux:n {x} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_clear_path:} +% \begin{macrocode} +\cs_new:Nn \hobby_clear_path: +{ +\array_gclear:N \g__hobby_points_array +\array_gclear:N \g__hobby_points_x_array +\array_gclear:N \g__hobby_points_y_array +\array_gclear:N \g__hobby_angles_array +\array_gclear:N \g__hobby_actions_array +\array_gclear:N \g__hobby_distances_array +\array_gclear:N \g__hobby_tension_out_array +\array_gclear:N \g__hobby_tension_in_array +\array_gclear:N \g__hobby_excess_angle_array +\array_gclear:N \g__hobby_matrix_a_array +\array_gclear:N \g__hobby_matrix_b_array +\array_gclear:N \g__hobby_matrix_c_array +\array_gclear:N \g__hobby_matrix_d_array +\array_gclear:N \g__hobby_vector_u_array +\array_gclear:N \g__hobby_psi_array +\array_gclear:N \g__hobby_theta_array +\array_gclear:N \g__hobby_phi_array +\array_gclear:N \g__hobby_sigma_array +\array_gclear:N \g__hobby_rho_array +\array_gclear:N \g__hobby_controla_array +\array_gclear:N \g__hobby_controlb_array +\bool_gset_false:N \g__hobby_closed_bool +\bool_gset_false:N \g__hobby_disjoint_bool + + \int_gset:Nn \g__hobby_npoints_int {-1} + \int_gset:Nn \g__hobby_draw_int {1} + \fp_gset_eq:NN \g__hobby_in_angle_fp \c_inf_fp + \fp_gset_eq:NN \g__hobby_out_angle_fp \c_inf_fp + \fp_gset_eq:NN \g__hobby_in_curl_fp \c_one_fp + \fp_gset_eq:NN \g__hobby_out_curl_fp \c_one_fp +} +% \end{macrocode} +% \end{macro} +% \begin{macrocode} +\ExplSyntaxOff +% \end{macrocode} +% \iffalse +%</hobby> +% \fi +% +% \subsection{PGF Library} +% +% \iffalse +%<*pgflibrary> +% \fi +% +% The PGF level is very simple. +% All we do is set up the path-construction commands that get passed to the path-generation function. +% \begin{macrocode} +\input{hobby.code.tex} +% \end{macrocode} +% Points are communicated as key-pairs. +% These keys translate from the \LaTeX3 style points to PGF points. +% \begin{macrocode} +\pgfkeys{ + /pgf/hobby/.is family, + /pgf/hobby/.cd, + point/.code={% + \hobby@parse@pt#1\relax} +} +\def\hobby@parse@pt#1,#2\relax{% + \pgf@x=#1cm\relax + \pgf@y=#2cm\relax +} +% \end{macrocode} +% +% \begin{macro}{hobbyatan2} +% The original PGF version of \Verb+atan2+ had the arguments the wrong way around. +% This was fixed in the CVS version in July 2013, but as old versions are likely to be in use for some time, we define a wrapper function that ensures that the arguments are correct. +% \begin{macrocode} +\pgfmathparse{atan2(0,1)} +\def\hobby@temp{0.0} +\ifx\pgfmathresult\hobby@temp + \pgfmathdeclarefunction{hobbyatan2}{2}{% + \pgfmathatantwo@{#1}{#2}% + } +\else + \pgfmathdeclarefunction{hobbyatan2}{2}{% + \pgfmathatantwo@{#2}{#1}% + } +\fi +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@curveto} +% This is passed to the path-generation code to translate the path into a PGF path. +% \begin{macrocode} +\def\hobby@curveto#1#2#3{% + \pgfpathcurveto{\hobby@topgf{#1}}{\hobby@topgf{#2}}{\hobby@topgf{#3}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@moveto} +% This is passed to the path-generation code to translate the path into a PGF path. +% \begin{macrocode} +\def\hobby@moveto#1#2#3{% + \pgfpathmoveto{\hobby@topgf{#3}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@topgf} +% Translates a \LaTeX3 point to a PGF point. +% \begin{macrocode} +\def\hobby@topgf#1{% + \pgfqkeys{/pgf/hobby}{point={#1}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@close} +% Closes a path. +% \begin{macrocode} +\def\hobby@close#1{% + \pgfpathclose +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgfpathhobby} +% Low-level interface to the hobby construction. +% This sets up the commands and starts the iterator. +% \begin{macrocode} +\def\pgfpathhobby{% + \pgfutil@ifnextchar\bgroup{\pgfpath@hobby}{\pgfpath@hobby{}}} +\def\pgfpath@hobby#1{% + \hobbyinit\hobby@moveto\hobby@curveto\hobby@close + \hobbysetparams{#1}% + \pgfmathsetmacro\hobby@x{\the\pgf@path@lastx/1cm}% + \pgfmathsetmacro\hobby@y{\the\pgf@path@lasty/1cm}% + \hobbyaddpoint{point={\hobby@x, \hobby@y}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgfpathhobbypt} +% Adds a point to the construction +% \begin{macrocode} +\def\pgfpathhobbypt#1{% + #1% + \pgfmathsetmacro\hobby@x{\the\pgf@x/1cm}% + \pgfmathsetmacro\hobby@y{\the\pgf@y/1cm}% + \pgfutil@ifnextchar\bgroup{\pgfpathhobbyptparams}{\pgfpathhobbyptparams{}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgfpathhobbyptparams} +% \begin{macrocode} +\def\pgfpathhobbyptparams#1{% + \hobbyaddpoint{#1,point={\hobby@x, \hobby@y}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgfpathhobbyend} +% \begin{macrocode} +\def\pgfpathhobbyend{% + \ifhobby@externalise + \ifx\hobby@path@name\pgfutil@empty + \hobbygenusepath + \else + \hobbygenuseifnecpath{\hobby@path@name}% + \fi + \else + \hobbygenusepath + \fi + \ifx\hobby@path@name\pgfutil@empty + \else + \hobbysavepath{\hobby@path@name}% + \fi + \global\let\hobby@path@name=\pgfutil@empty +} +% \end{macrocode} +% \end{macro} +% +% +% Plot handlers +% +% \begin{macro}{\pgfplothanderhobby} +% Basic plot handler; uses full algorithm but therefore expensive +% \begin{macrocode} +\def\pgfplothandlerhobby{% + \def\pgf@plotstreamstart{% + \hobbyinit\hobby@moveto\hobby@curveto\hobby@close + \global\let\pgf@plotstreampoint=\pgf@plot@hobby@firstpt + \global\let\pgf@plotstreamspecial=\pgfutil@gobble + \gdef\pgf@plotstreamend{% + \ifhobby@externalise + \ifx\hobby@path@name\pgfutil@empty + \hobbygenusepath + \else + \hobbygenuseifnecpath{\hobby@path@name}% + \fi + \else + \hobbygenusepath + \fi + \ifx\hobby@path@name\pgfutil@empty + \else + \hobbysavepath{\hobby@path@name}% + \fi + \global\let\hobby@path@name=\pgfutil@empty + }% + \let\tikz@scan@point@options=\pgfutil@empty + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgfplothandlerclosedhobby} +% Same as above but produces a closed curve +% \begin{macrocode} +\def\pgfplothandlerclosedhobby{% + \def\pgf@plotstreamstart{% + \hobbyinit\hobby@moveto\hobby@curveto\hobby@close + \hobbysetparams{closed=true,disjoint=true}% + \global\let\pgf@plotstreampoint=\pgf@plot@hobby@firstpt + \global\let\pgf@plotstreamspecial=\pgfutil@gobble + \gdef\pgf@plotstreamend{% + \ifhobby@externalise + \ifx\hobby@path@name\pgfutil@empty + \hobbygenusepath + \else + \hobbygenuseifnecpath{\hobby@path@name}% + \fi + \else + \hobbygenusepath + \fi + \ifx\hobby@path@name\pgfutil@empty + \else + \hobbysavepath{\hobby@path@name}% + \fi + \global\let\hobby@path@name=\pgfutil@empty + }% + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgf@plot@hobby@firstpt} +% First point, move or line as appropriate and then start the algorithm. +% \begin{macrocode} +\def\pgf@plot@hobby@firstpt#1{% + \pgf@plot@first@action{#1}% + \pgf@plot@hobby@handler{#1}% + \global\let\pgf@plotstreampoint=\pgf@plot@hobby@handler +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgf@plot@hobby@handler} +% Add points to the array for the algorithm to work on. +% \begin{macrocode} +\def\pgf@plot@hobby@handler#1{% + #1% + \pgfmathsetmacro\hobby@x{\the\pgf@x/1cm}% + \pgfmathsetmacro\hobby@y{\the\pgf@y/1cm}% + \hobbyaddpoint{point={\hobby@x, \hobby@y}}% + } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgfplothandlerquickhobby} +% Uses the ``quick'' algorithm. +% \begin{macrocode} +\def\pgfplothandlerquickhobby{% + \def\pgf@plotstreamstart{% + \global\let\hobby@quick@curveto=\pgfpathcurveto + \global\let\pgf@plotstreampoint=\pgf@plot@qhobby@firstpt + \global\let\pgf@plotstreamspecial=\pgfutil@gobble + \global\let\pgf@plotstreamend=\pgf@plot@qhobby@end + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgf@plot@qhobby@firstpt} +% Carry out first action (move or line) and save point. +% \begin{macrocode} +\def\pgf@plot@qhobby@firstpt#1{% + #1% + \edef\hobby@temp{\noexpand\pgf@plot@first@action{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}}\hobby@temp% + \xdef\hobby@qpoints{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \gdef\hobby@qpointa{}% + \gdef\hobby@angle{}% + \global\let\pgf@plotstreampoint=\pgf@plot@qhobby@secondpt +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgf@plot@qhobby@secondpt} +% Also need to save second point. +% \begin{macrocode} +\def\pgf@plot@qhobby@secondpt#1{% + #1% + \xdef\hobby@qpointa{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \global\let\pgf@plotstreampoint=\pgf@plot@qhobby@handler +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgf@plot@qhobby@handler} +% Wrapper around the computation macro that saves the variables globally. +% \begin{macrocode} +\def\pgf@plot@qhobby@handler#1{% + #1 + \edef\hobby@temp{\noexpand\hobby@quick@compute{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}}\hobby@temp + \global\let\hobby@qpointa=\hobby@qpointa + \global\let\hobby@qpoints=\hobby@qpoints + \global\let\hobby@angle=\hobby@angle +% \end{macrocode} +% Also need to save some data for the last point +% \begin{macrocode} + \global\let\hobby@thetaone=\hobby@thetaone + \global\let\hobby@phitwo=\hobby@phitwo + \global\let\hobby@done=\hobby@done + \global\let\hobby@omegaone=\hobby@omegaone +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\pgf@plot@qhobby@end} +% Wrapper around the finalisation step. +% \begin{macrocode} +\def\pgf@plot@qhobby@end{% + \hobby@quick@computeend +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@sf} +% Working with points leads to computations out of range so we scale to get them into the computable arena. +% \begin{macrocode} +\pgfmathsetmacro\hobby@sf{10cm} +% \end{macrocode} +% \end{macro} +% +% +% \begin{macro}{\hobby@quick@compute} +% This is the macro that does all the work of computing the control points. +% The argument is the current point, \Verb+\hobby@qpointa+ is the middle point, and \Verb+\hobby@qpoints+ is the first point. +% \begin{macrocode} +\def\hobby@quick@compute#1{% +% \end{macrocode} +% Save the current (second - counting from zero) point in \Verb+\pgf@xb+ and \Verb+\pgf@yb+. +% \begin{macrocode} + #1% + \pgf@xb=\pgf@x + \pgf@yb=\pgf@y +% \end{macrocode} +% Save the previous (first) point in \Verb+\pgf@xa+ and \Verb+\pgf@ya+. +% \begin{macrocode} + \hobby@qpointa + \pgf@xa=\pgf@x + \pgf@ya=\pgf@y +% \end{macrocode} +% Adjust so that \Verb+(\pgf@xb,\pgf@yb)+ is the vector from second to third. +% Then compute and store the distance and angle of this vector. +% We view this as the vector \emph{from} the midpoint and everything to do with that point has the suffix \Verb+one+. +% Note that we divide by the scale factor here. +% \begin{macrocode} + \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(hobbyatan2(\pgf@yb,\pgf@xb))}% +% \end{macrocode} +% Now we do the same with the vector from the zeroth to the first point. +% \begin{macrocode} + \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(hobbyatan2(\pgf@ya,\pgf@xa))}% +% \end{macrocode} +% \Verb+\hobby@psi+ is the angle subtended at the midpoint. +% We adjust to ensure that it is in the right range. +% \begin{macrocode} + \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}% +% \end{macrocode} +% Now we test to see if we're on the first run or not. +% If the first, we have no incoming angle. +% \begin{macrocode} + \ifx\hobby@angle\pgfutil@empty +% \end{macrocode} +% First. +% \begin{macrocode} + \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 +% \end{macrocode} +% Second or later. +% \begin{macrocode} + \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 +% \end{macrocode} +% Save the outgoing angle. +% \begin{macrocode} + \let\hobby@angle=\hobby@thetaone +% \end{macrocode} +% Compute the control points from the angles. +% \begin{macrocode} + \hobby@quick@ctrlpts{\hobby@thetazero}{\hobby@phione}{\hobby@qpoints}{\hobby@qpointa}{\hobby@dzero}{\hobby@omegazero}% +% \end{macrocode} +% Now call the call-back function +% \begin{macrocode} + \edef\hobby@temp{\noexpand\hobby@quick@curveto{\noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}{\noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}}% +\hobby@temp +% \end{macrocode} +% Cycle the points round for the next iteration. +% \begin{macrocode} + \global\let\hobby@qpoints=\hobby@qpointa + #1 + \xdef\hobby@qpointa{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% +% \end{macrocode} +% Save needed values in global macros +% \begin{macrocode} + \global\let\hobby@angle=\hobby@angle + \global\let\hobby@phitwo=\hobby@phitwo + \global\let\hobby@thetaone=\hobby@thetaone +\global\let\hobby@done=\hobby@done +\global\let\hobby@omegaone=\hobby@omegaone +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@wuick@computeend} +% This is the additional code for the final run. +% \begin{macrocode} +\def\hobby@quick@computeend{% +% \end{macrocode} +% Compute the control points for the second part of the curve and add that to the path. +% \begin{macrocode} + \hobby@quick@ctrlpts{\hobby@thetaone}{\hobby@phitwo}{\hobby@qpoints}{\hobby@qpointa}{\hobby@done}{\hobby@omegaone}% +% \end{macrocode} +% Now call the call-back function +% \begin{macrocode} + \edef\hobby@temp{\noexpand\hobby@quick@curveto{\noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}{\noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}}% +\hobby@temp +}% +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@quick@ctrlpts} +% Compute the control points from the angles and points given. +% \begin{macrocode} +\def\hobby@quick@ctrlpts#1#2#3#4#5#6{% + \pgfmathsetmacro\hobby@alpha{% + sqrt(2) * (sin(#1 r) - 1/16 * sin(#2 r))% +* (sin(#2 r) - 1/16 * sin(#1 r))% + * (cos(#1 r) - cos(#2 r))}% + \pgfmathsetmacro\hobby@rho{% + (2 + \hobby@alpha)/(1 + (1 - (3 - sqrt(5))/2)% + * cos(#1 r) + (3 - sqrt(5))/2 * cos(#2 r))}% + \pgfmathsetmacro\hobby@sigma{% + (2 - \hobby@alpha)/(1 + (1 - (3 - sqrt(5))/2)% + * cos(#2 r) + (3 - sqrt(5))/2 * cos(#1 r))}% + #3% + \pgf@xa=\pgf@x + \pgf@ya=\pgf@y + \pgfmathsetlength\pgf@xa{% + \pgf@xa + #5 * \hobby@rho% + * cos((#1 + #6) r)/3*\hobby@sf}% + \pgfmathsetlength\pgf@ya{% + \pgf@ya + #5 * \hobby@rho% + * sin((#1 + #6) r)/3*\hobby@sf}% + #4% + \pgf@xb=\pgf@x + \pgf@yb=\pgf@y + \pgfmathsetlength\pgf@xb{% + \pgf@xb - #5 * \hobby@sigma% + * cos((-#2 + #6) r)/3*\hobby@sf}% + \pgfmathsetlength\pgf@yb{% + \pgf@yb - #5 * \hobby@sigma% + * sin((-#2 + #6) r)/3*\hobby@sf}% + #4% +} +% \end{macrocode} +% \end{macro} +% +% \iffalse +%</pgflibrary> +% \fi +% +% \subsection{TikZ Library} +% +% \iffalse +%<*tikzlibrary> +% \fi +% +% \begin{macrocode} +\usepgflibrary{hobby} +\let\hobby@this@opts=\pgfutil@empty +\let\hobby@next@opts=\pgfutil@empty +\let\hobby@action=\pgfutil@empty +\let\hobby@path@name=\pgfutil@empty +\newif\ifhobby@externalise +% \end{macrocode} +% +% We set various TikZ keys. +% These include the \Verb+to path+ constructor and all the various parameters that will eventually get passed to the path-generation code. +% \begin{macrocode} +\def\hobby@point@options{}% +\tikzset{ + curve through/.style={ + to path={ + \pgfextra{ + \expandafter\curvethrough\expandafter[\hobby@next@opts]{% + (\tikztostart) .. #1 .. (\tikztotarget)% + } + } + } + }, + tension in/.code = {% + \expandafter\gdef\expandafter\hobby@point@options\expandafter% + {\hobby@point@options,tension in=#1}% + }, + tension out/.code = {% + \expandafter\gdef\expandafter\hobby@point@options\expandafter% + {\hobby@point@options,tension out=#1}% + }, + tension/.append code = {% + \expandafter\gdef\expandafter\hobby@point@options\expandafter% + {\hobby@point@options,tension=#1}% + }, + excess angle/.code = {% + \expandafter\gdef\expandafter\hobby@point@options\expandafter% + {\hobby@point@options,excess angle=#1}% + }, + break/.code = {% + \expandafter\gdef\expandafter\hobby@point@options\expandafter% + {\hobby@point@options,break=#1}% + }, + blank/.code = {% + \expandafter\gdef\expandafter\hobby@point@options\expandafter% + {\hobby@point@options,blank=#1}% + }, + designated Hobby path/.initial={next}, + clear next Hobby path options/.code={% + \gdef\hobby@next@opts{}% + }, + clear this Hobby path options/.code={% + \gdef\hobby@this@opts{}% + }, + clear Hobby path options/.style={% + clear \pgfkeysvalueof{/tikz/designated Hobby path} Hobby path options + }, + add option to this Hobby path/.code={% + \expandafter\gdef\expandafter\hobby@this@opts\expandafter{\hobby@this@opts#1,}% + }, + add option to next Hobby path/.code={% + \expandafter\gdef\expandafter\hobby@next@opts\expandafter{\hobby@next@opts#1,}% + }, + add option to Hobby path/.style={% + add option to \pgfkeysvalueof{/tikz/designated Hobby path} Hobby path={#1}% + }, + closed/.style = {% + add option to Hobby path={closed=#1,disjoint=#1}% + }, + invert blank/.style = {% + add option to Hobby path={invert blank=#1}% + }, + closed/.default = true, + blank/.default = true, + break/.default = true, + invert blank/.default = true, + in angle/.code = {% + \pgfmathparse{(#1)*pi/180}% + \edef\@temp{in angle=\pgfmathresult,}% + \pgfkeysalso{add option to Hobby path/.expand once=\@temp}% + }, + out angle/.code = {% + \pgfmathparse{(#1)*pi/180}% + \edef\@temp{out angle=\pgfmathresult,}% + \pgfkeysalso{add option to Hobby path/.expand once=\@temp}% + }, + in curl/.style = {% + add option to Hobby path={in curl=#1}% + }, + out curl/.style = {% + add option to Hobby path={out curl=#1}% + }, + use Hobby shortcut/.code={% + \let\tikz@curveto@auto=\hobby@curveto@override + \global\let\hobby@curveto@delegate=\hobby@curveto@auto + }, + use quick Hobby shortcut/.code={% + \let\tikz@curveto@auto=\hobby@curveto@override + \global\let\hobby@curveto@delegate=\hobby@qcurveto@auto + }, + use previous Hobby path/.code={% + \hobbyusepath{#1}% + }, + use previous Hobby path/.default={},% + save Hobby path/.code={% + \xdef\hobby@path@name{#1}% + }, + restore Hobby path/.code={% + \hobbyinit\hobby@tikz@moveto\hobby@tikz@curveto\hobby@tikz@close + \global\let\hobby@collected@onpath\pgfutil@empty + \hobbyrestorepath{#1}% + }, + restore and use Hobby path/.code 2 args={% + \hobbyinit\hobby@tikz@moveto\hobby@tikz@curveto\hobby@tikz@close + \global\let\hobby@collected@onpath\pgfutil@empty + \hobbyrestorepath{#1}% + \hobbyusepath{#2}% + }, + show Hobby path/.code={% + \hobbyshowpath{#1}% + }, + Hobby action/.code={% + \expandafter\gdef\expandafter\hobby@action\expandafter{\hobby@action#1}% + }, + Hobby finish/.style={% + Hobby action=\hobby@finish% + }, + Hobby externalise/.is if=hobby@externalise, + Hobby externalize/.is if=hobby@externalise +} +% \end{macrocode} +% +% \begin{macro}{\hobby@tikz@curveto} +% This is passed to the path-generation code to translate the path into a PGF path. +% \begin{macrocode} +\def\hobby@tikz@curveto#1#2#3{% + \pgfutil@ifundefined{tikz@timer@start}{% + \expandafter\hobby@topgf\expandafter{\hobby@initial@pt}% + \edef\tikz@timer@start{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + }{}% + \hobby@topgf{#1}% + \edef\tikz@timer@cont@one{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \hobby@topgf{#2}% + \edef\tikz@timer@cont@two{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \hobby@topgf{#3}% + \let\tikz@timer=\tikz@timer@curve + \edef\tikz@timer@end{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \ifx\hobby@collected@onpath\pgfutil@empty + \else + \expandafter\hobby@nodes@onpath\hobby@collected@onpath\relax\relax + \fi + \pgfpathcurveto{\hobby@topgf{#1}}{\hobby@topgf{#2}}{\hobby@topgf{#3}}% + \hobby@topgf{#3}% + \edef\tikz@timer@start{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@tikz@moveto} +% This is passed to the path-generation code to translate the path into a PGF path. +% \begin{macrocode} +\def\hobby@tikz@moveto#1#2#3{% + \pgfutil@ifundefined{tikz@timer@start}{% + \expandafter\hobby@topgf\expandafter{\hobby@initial@pt}% + \edef\tikz@timer@start{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + }{}% + \hobby@topgf{#3}% + \edef\tikz@timer@end{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \def\pgf@temp{#1}% + \ifx\pgf@temp\pgfutil@empty + \let\tikz@timer=\tikz@timer@line + \expandafter\def\expandafter\hobby@collected@onpath\expandafter{\expandafter{\expandafter}\hobby@collected@onpath} + \else + \hobby@topgf{#1}% + \edef\tikz@timer@cont@one{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \hobby@topgf{#2}% + \edef\tikz@timer@cont@two{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \let\tikz@timer=\tikz@timer@curve + \fi + \ifx\hobby@collected@onpath\pgfutil@empty + \else + \expandafter\hobby@nodes@onpath\hobby@collected@onpath\relax\relax + \fi + \pgfpathmoveto{\hobby@topgf{#3}}% + \hobby@topgf{#3}% + \edef\tikz@timer@start{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@tikz@close} +% Closes a path. +% \begin{macrocode} +\def\hobby@tikz@close#1{% + \hobby@topgf{#1}% + \edef\tikz@timer@end{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \let\tikz@timer=\tikz@timer@line + \ifx\hobby@collected@onpath\pgfutil@empty + \else + \expandafter\hobby@nodes@onpath\hobby@collected@onpath\relax\relax + \fi + \pgfpathclose +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@nodes@onpath} +% \begin{macrocode} +\def\hobby@nodes@onpath#1#2\relax{% + \gdef\hobby@collected@onpath{#2}% + \def\pgf@temp{#1}% + \ifx\pgf@temp\pgfutil@empty + \else + \def\@gtempa{\relax} + \ifx\pgf@temp\@gtempa + \else + \tikz@node@is@a@labeltrue + \tikz@scan@next@command#1\pgf@stop + \tikz@node@is@a@labelfalse + \fi + \fi +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\curvethrough} +% This is the parent command. +% We initialise the path-generation code, set any parameters, and then hand over control to the point processing macro. +% \begin{macrocode} +\newcommand\curvethrough[2][]{% + \hobbyinit\hobby@tikz@moveto\hobby@tikz@curveto\hobby@tikz@close + \global\let\hobby@collected@onpath\pgfutil@empty + \let\hobby@initial@pt\pgfutil@empty + \hobbysetparams{#1}% + \tikzset{designated Hobby path=this}% + \global\let\hobby@this@opts=\pgfutil@empty + \global\let\hobby@next@opts=\pgfutil@empty + \let\tikz@scan@point@options=\pgfutil@empty + \def\hobby@point@options{}% + \tikz@scan@one@point\hobby@processpt #2 \relax% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@processpt} +% This processes a list of points in the format \Verb+(0,0) [..] (1,1)+. +% Each point is scanned by TikZ and then added to the stack to be built into the path. +% If there are any remaining points, we call ourself again with them. +% Otherwise, we hand over control to the path-generation code. +% \begin{macrocode} +\newcommand\hobby@processpt[1]{% + #1% + \pgfmathsetmacro\hobby@x{\the\pgf@x/1cm}% + \pgfmathsetmacro\hobby@y{\the\pgf@y/1cm}% + \ifx\hobby@initial@pt\pgfutil@empty + \xdef\hobby@initial@pt{\hobby@x, \hobby@y}% + \fi + \expandafter\hobbyaddpoint\expandafter{\hobby@point@options,% + point={\hobby@x, \hobby@y}}% + \def\hobby@point@options{}% + \let\tikz@scan@point@options=\pgfutil@empty + \pgfutil@ifnextchar\relax{% + \expandafter\hobbysetparams\expandafter{\hobby@this@opts}% + \ifhobby@externalise + \ifx\hobby@path@name\pgfutil@empty + \hobbygenusepath + \else + \hobbygenuseifnecpath{\hobby@path@name}% + \fi + \else + \hobbygenusepath + \fi + \ifx\hobby@path@name\pgfutil@empty + \else + \hobbysavepath{\hobby@path@name}% + \fi + \global\let\hobby@path@name=\pgfutil@empty + }{% + \pgfutil@ifnextchar.{% + \hobby@swallowdots}{% + \tikz@scan@one@point\hobby@processpt}}} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@swallowdots} +% Remove dots from the input stream. +% \begin{macrocode} +\def\hobby@swallowdots.{% + \pgfutil@ifnextchar.{% + \hobby@swallowdots}{% + \tikz@scan@one@point\hobby@processpt}} +% \end{macrocode} +% \end{macro} +% +% There is a ``spare hook'' in the TikZ path processing code. +% If TikZ encounters a path of the form \Verb+(0,0) .. (1,1)+ then it calls a macro \Verb+\tikz@curveto@auto+. +% However, that macro is not defined in the TikZ code. +% The following code provides a suitable definition. +% To play nice, we don't install it by default but define a key (defined above) that installs it. +% +% \begin{macro}{\hobby@curveto@override} +% \begin{macrocode} +\def\hobby@curveto@override{% + \hobby@curveto@delegate} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@curveto@auto} +% When we're called by TikZ, we initialise the path generation code and start adding points. +% To ensure that the generation code is called, we add a lot of hooks to lots of TikZ commands. +% \begin{macrocode} +\def\hobby@curveto@auto{% + \hobbyinit\hobby@tikz@moveto\hobby@tikz@curveto\hobby@tikz@close + \expandafter\gdef\expandafter\hobby@collected@onpath\expandafter{\expandafter{\tikz@collected@onpath} }% + \let\tikz@collected@onpath=\pgfutil@empty + \pgfmathsetmacro\hobby@x{\the\tikz@lastx/1cm}% + \pgfmathsetmacro\hobby@y{\the\tikz@lasty/1cm}% + \xdef\hobby@initial@pt{\hobby@x, \hobby@y}% + \expandafter\hobbysetparams\expandafter{\hobby@next@opts}% + \expandafter\hobbyaddpoint\expandafter{\hobby@point@options,% + point={\hobby@x, \hobby@y} }% + \hobby@init@tikz@commands + \tikzset{designated Hobby path=this}% + \let\tikz@scan@point@options=\pgfutil@empty + \global\let\hobby@action=\pgfutil@empty + \global\let\hobby@this@opts=\pgfutil@empty + \global\let\hobby@next@opts=\pgfutil@empty + \global\let\hobby@point@options=\pgfutil@empty + \tikz@scan@one@point\hobby@addfromtikz% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@addfromtikz} +% This adds our current point to the stack. +% \begin{macrocode} +\def\hobby@addfromtikz#1{% + #1% + \tikz@make@last@position{#1}% + \pgfmathsetmacro\hobby@x{\the\pgf@x/1cm}% + \pgfmathsetmacro\hobby@y{\the\pgf@y/1cm}% + \expandafter\hobbysetparams\expandafter{\hobby@this@opts}% + \expandafter\hobbyaddpoint\expandafter{\hobby@point@options,% + point={\hobby@x, \hobby@y}}% + \hobby@action + \global\let\hobby@this@opts=\pgfutil@empty + \global\let\hobby@action=\pgfutil@empty + \global\let\hobby@point@options=\pgfutil@empty + \tikz@scan@next@command% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@init@tikz@commands} +% \begin{macrocode} +\def\hobby@init@tikz@commands{% + \hobby@init@tikz@modcmd\tikz@movetoabs + \hobby@init@tikz@modcmd\tikz@movetorel + \hobby@init@tikz@modcmd\tikz@lineto + \hobby@init@tikz@modcmd\tikz@rect + \hobby@init@tikz@modcmd\tikz@cchar + \hobby@init@tikz@modcmd\tikz@finish + \hobby@init@tikz@modcmd\tikz@arcA + \hobby@init@tikz@modcmd\tikz@e@char + \hobby@init@tikz@modcmd\tikz@g@char + \hobby@init@tikz@modcmd\tikz@schar + \hobby@init@tikz@modcmd\tikz@vh@lineto + \hobby@init@tikz@modcmd\tikz@pchar + \hobby@init@tikz@modcmd\tikz@to + \hobby@init@tikz@modcmd\pgf@stop + \hobby@init@tikz@modcmd\tikz@decoration + \global\let\hobby@curveto@delegate=\hobby@midcurveto@auto +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@restore@tikz@commands} +% \begin{macrocode} +\def\hobby@restore@tikz@commands{% + \hobby@restore@tikz@modcmd\tikz@movetoabs + \hobby@restore@tikz@modcmd\tikz@movetorel + \hobby@restore@tikz@modcmd\tikz@lineto + \hobby@restore@tikz@modcmd\tikz@rect + \hobby@restore@tikz@modcmd\tikz@cchar + \hobby@restore@tikz@modcmd\tikz@finish + \hobby@restore@tikz@modcmd\tikz@arcA + \hobby@restore@tikz@modcmd\tikz@e@char + \hobby@restore@tikz@modcmd\tikz@g@char + \hobby@restore@tikz@modcmd\tikz@schar + \hobby@restore@tikz@modcmd\tikz@vh@lineto + \hobby@restore@tikz@modcmd\tikz@pchar + \hobby@restore@tikz@modcmd\tikz@to + \hobby@restore@tikz@modcmd\pgf@stop + \hobby@restore@tikz@modcmd\tikz@decoration + \global\let\hobby@curveto@delegate=\hobby@curveto@auto +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@init@tikz@modcmd} +% \begin{macrocode} +\def\hobby@init@tikz@modcmd#1{% + \expandafter\global\expandafter\let\csname hobby@orig@\string#1\endcsname=#1% + \gdef#1{\hobby@finish#1}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@restore@tikz@modcmd} +% \begin{macrocode} +\def\hobby@restore@tikz@modcmd#1{% + \expandafter\global\expandafter\let\expandafter#1\csname hobby@orig@\string#1\endcsname% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@midcurveto@auto} +% \begin{macrocode} +\def\hobby@midcurveto@auto{% + \expandafter\expandafter\expandafter\gdef\expandafter\expandafter\expandafter\hobby@collected@onpath\expandafter\expandafter\expandafter{\expandafter\hobby@collected@onpath\expandafter{\tikz@collected@onpath} }% + \let\tikz@collected@onpath=\pgfutil@empty + \let\tikz@scan@point@options=\pgfutil@empty + \global\let\hobby@action=\pgfutil@empty + \global\let\hobby@this@opts=\pgfutil@empty + \global\let\hobby@point@options=\pgfutil@empty + \tikz@scan@one@point\hobby@addfromtikz% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@finish} +% \begin{macrocode} +\def\hobby@finish{% + \hobby@restore@tikz@commands + \ifhobby@externalise + \ifx\hobby@path@name\pgfutil@empty + \hobbygenusepath + \else + \hobbygenuseifnecpath{\hobby@path@name}% + \fi + \else + \hobbygenusepath + \fi + \ifx\hobby@path@name\pgfutil@empty + \else + \hobbysavepath{\hobby@path@name}% + \fi + \global\let\hobby@path@name=\pgfutil@empty + \tikzset{designated Hobby path=next}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{quick curve through} +% The \Verb+quick curve through+ is a \Verb+to path+ which does the ``quick'' version of Hobby's algorithm. +% The syntax is as with the \Verb+curve through+: to pass the midpoints as the argument to the style. +% We need to pass three points to the auxiliary macro. +% These are passed as \Verb+\hobby@qpoints+, \Verb+\hobby@qpointa+, and the current point. +% Then these get cycled round for the next triple. +% The path gets built up and stored as \Verb+\hobby@quick@path+. +% We also have to remember the angle computed for the next round. +% \begin{macrocode} +\tikzset{ + quick curve through/.style={% + to path={% + \pgfextra{% +% \end{macrocode} +% Scan the starting point and store the coordinates in \Verb+\hobby@qpointa+ +% \begin{macrocode} + \let\hobby@next@qbreak=\relax + \let\hobby@next@qblank=\relax + \tikz@scan@one@point\pgfutil@firstofone(\tikztostart)% + \tikz@make@last@position{\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \edef\hobby@qpoints{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% +% \end{macrocode} +% Blank the path and auxiliary macros. +% \begin{macrocode} + \def\hobby@qpointa{}% + \def\hobby@quick@path{}% + \def\hobby@angle{}% + \let\hobby@quick@curveto=\hobby@quick@makepath +% \end{macrocode} +% Now start parsing the rest of the coordinates. +% \begin{macrocode} + \tikz@scan@one@point\hobby@quickfirst #1 (\tikztotarget)\relax + } +% \end{macrocode} +% Invoke the path +% \begin{macrocode} + \hobby@quick@path + } + }, + quick hobby/blank curve/.is choice, + quick hobby/blank curve/true/.code={% + \gdef\hobby@next@qblank{% + \qhobby@blanktrue + \global\let\hobby@next@qblank=\relax + }% + }, + quick hobby/blank curve/false/.code={% + \gdef\hobby@next@qblank{% + \qhobby@blankfalse + \global\let\hobby@next@qblank=\relax + }% + }, + quick hobby/blank curve/once/.code={% + \gdef\hobby@next@qblank{% + \qhobby@blanktrue + \gdef\hobby@next@qblank{% + \qhobby@blankfalse + \global\let\hobby@next@qblank=\relax + }% + }% + }, + quick hobby/blank curve/.default=true, + quick hobby/break curve/.is choice, + quick hobby/break curve/true/.code={% + \gdef\hobby@next@qbreak{% + \qhobby@breaktrue + \global\let\hobby@next@qbreak=\relax + }% + }, + quick hobby/break curve/false/.code={% + \gdef\hobby@next@qbreak{% + \qhobby@breakfalse + \global\let\hobby@next@qbreak=\relax + }% + }, + quick hobby/break curve/once/.code={% + \gdef\hobby@next@qbreak{% + \qhobby@breaktrue + \gdef\hobby@next@qbreak{% + \qhobby@breakfalse + \global\let\hobby@next@qbreak=\relax + }% + }% + }, + quick hobby/break curve/.default=true, +} +\newif\ifqhobby@break +\newif\ifqhobby@blank +% \end{macrocode} +% \end{macro} +% +% Add plot handlers +% \begin{macrocode} +\tikzoption{hobby}[]{\let\tikz@plot@handler=\pgfplothandlerhobby} +\tikzoption{quick hobby}[]{\let\tikz@plot@handler=\pgfplothandlerquickhobby} +\tikzoption{closed hobby}[]{\let\tikz@plot@handler=\pgfplothandlerclosedhobby} +% \end{macrocode} +% +% \begin{macro}{\hobby@quickfirst} +% The first time around we just set the next point. +% \begin{macrocode} +\def\hobby@quickfirst#1{% + #1% + \xdef\hobby@qpointa{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \tikz@make@last@position{\hobby@qpointa}% +% \end{macrocode} +% Now a check to ensure that we have more points. +% \begin{macrocode} + \pgfutil@ifnextchar\relax{% +% \end{macrocode} +% Ooops, no more points. +% That's not good. +% Bail-out. +% \begin{macrocode} + \xdef\hobby@quick@path{ -- (\the\pgf@x,\the\pgf@y)}% + }{% +% \end{macrocode} +% Okay, have more points. +% Phew. +% Call the next round. +% If we have dots, swallow them. +% \begin{macrocode} + \pgfutil@ifnextchar.{% + \hobby@qswallowdots}{% + \tikz@scan@one@point\hobby@quick}}} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qswallowdots} +% Remove dots from the input stream. +% \begin{macrocode} +\def\hobby@qswallowdots.{% + \pgfutil@ifnextchar.{% + \hobby@qswallowdots}{% + \tikz@scan@one@point\hobby@quick}} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@quick} +% This is our wrapper function that handles the loop. +% \begin{macrocode} +\def\hobby@quick#1{% + \hobby@quick@compute{#1}% + \tikz@make@last@position{\hobby@qpointa}% + \pgfutil@ifnextchar\relax{% +% \end{macrocode} +% End of loop +% \begin{macrocode} + \hobby@quick@computeend% + }{% +% \end{macrocode} +% More to go, scan in the next coordinate and off we go again. +% \begin{macrocode} + \pgfutil@ifnextchar.{% + \hobby@qswallowdots}{% + \tikz@scan@one@point\hobby@quick}}} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@quick@makepath} +% Path constructor for \Verb+to path+ use. +% \begin{macrocode} +\def\hobby@quick@makepath#1#2#3{% + #1% + \pgf@xa=\pgf@x\relax + \pgf@ya=\pgf@y\relax + #2% + \pgf@xb=\pgf@x\relax + \pgf@yb=\pgf@y\relax + #3% + \ifqhobby@blank + \xdef\hobby@quick@path{\hobby@quick@path (\the\pgf@x,\the\pgf@y)}% + \else + \xdef\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 + \ifqhobby@break + \xdef\hobby@quick@path{\hobby@quick@path +(0,0)}% + \fi + \hobby@next@qbreak + \hobby@next@qblank +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qcurveto@auto} +% Uses the ``quick'' method for the shortcut syntax. +% \begin{macrocode} +\def\hobby@qcurveto@auto{% + \global\let\hobby@next@qbreak=\relax + \global\let\hobby@next@qblank=\relax + \xdef\hobby@qpoints{\noexpand\pgfqpoint{\the\tikz@lastx}{\the\tikz@lasty}}% + \gdef\hobby@qpointa{}% + \gdef\hobby@quick@path{}% + \gdef\hobby@angle{}% + \global\let\hobby@quick@curveto=\hobby@quick@makepathauto + \hobby@qinit@tikz@commands + \let\tikz@scan@point@options=\pgfutil@empty + \global\let\hobby@action=\pgfutil@empty + \global\let\hobby@point@options=\pgfutil@empty + \tikz@scan@one@point\hobby@qfirst@auto} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qmidcurveto@auto} +% \begin{macrocode} +\def\hobby@qmidcurveto@auto{% + \let\tikz@scan@point@options=\pgfutil@empty + \global\let\hobby@action=\pgfutil@empty + \global\let\hobby@point@options=\pgfutil@empty + \tikz@scan@one@point\hobby@qaddfromtikz} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qfirst@auto} +% \begin{macrocode} +\def\hobby@qfirst@auto#1{% + #1% + \xdef\hobby@qpointa{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + \tikz@make@last@position{\hobby@qpointa}% + \tikz@scan@next@command% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@quick@makepathauto} +% Path constructor for shortcut method to use. +% \begin{macrocode} +\def\hobby@quick@makepathauto#1#2#3{% + #1% + \pgf@xa=\pgf@x\relax + \pgf@ya=\pgf@y\relax + #2% + \pgf@xb=\pgf@x\relax + \pgf@yb=\pgf@y\relax + #3% + \ifqhobby@blank + \edef\hobby@temp{% + \noexpand\pgfpathmoveto{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + }% + \hobby@temp + \else + \edef\hobby@temp{% + \noexpand\pgfpathcurveto{\noexpand\pgfqpoint{\the\pgf@xa}{\the\pgf@ya}}% + {\noexpand\pgfqpoint{\the\pgf@xb}{\the\pgf@yb}}% + {\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + }% + \hobby@temp + \fi + \ifqhobby@break + #3% + \edef\hobby@temp{% + \noexpand\pgfpathmoveto{\noexpand\pgfqpoint{\the\pgf@x}{\the\pgf@y}}% + }% + \hobby@temp + \fi + \hobby@next@qbreak + \hobby@next@qblank +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qaddfromtikz} +% This adds our current point to the stack. +% \begin{macrocode} +\def\hobby@qaddfromtikz#1{% + \hobby@quick@compute{#1}% + \tikz@make@last@position{\hobby@qpointa}% + \tikz@scan@next@command% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qinit@tikz@commands} +% \begin{macrocode} +\def\hobby@qinit@tikz@commands{% + \hobby@qinit@tikz@modcmd\tikz@movetoabs + \hobby@qinit@tikz@modcmd\tikz@movetorel + \hobby@qinit@tikz@modcmd\tikz@lineto + \hobby@qinit@tikz@modcmd\tikz@rect + \hobby@qinit@tikz@modcmd\tikz@cchar + \hobby@qinit@tikz@modcmd\tikz@finish + \hobby@qinit@tikz@modcmd\tikz@arcA + \hobby@qinit@tikz@modcmd\tikz@e@char + \hobby@qinit@tikz@modcmd\tikz@g@char + \hobby@qinit@tikz@modcmd\tikz@schar + \hobby@qinit@tikz@modcmd\tikz@vh@lineto + \hobby@qinit@tikz@modcmd\tikz@pchar + \hobby@qinit@tikz@modcmd\tikz@to + \hobby@qinit@tikz@modcmd\pgf@stop + \hobby@qinit@tikz@modcmd\tikz@decoration + \hobby@qinit@tikz@modcmd\tikz@@close + \global\let\hobby@curveto@delegate=\hobby@qmidcurveto@auto +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qrestore@tikz@commands} +% \begin{macrocode} +\def\hobby@qrestore@tikz@commands{% + \hobby@restore@tikz@modcmd\tikz@movetoabs + \hobby@restore@tikz@modcmd\tikz@movetorel + \hobby@restore@tikz@modcmd\tikz@lineto + \hobby@restore@tikz@modcmd\tikz@rect + \hobby@restore@tikz@modcmd\tikz@cchar + \hobby@restore@tikz@modcmd\tikz@finish + \hobby@restore@tikz@modcmd\tikz@arcA + \hobby@restore@tikz@modcmd\tikz@e@char + \hobby@restore@tikz@modcmd\tikz@g@char + \hobby@restore@tikz@modcmd\tikz@schar + \hobby@restore@tikz@modcmd\tikz@vh@lineto + \hobby@restore@tikz@modcmd\tikz@pchar + \hobby@restore@tikz@modcmd\tikz@to + \hobby@restore@tikz@modcmd\pgf@stop + \hobby@restore@tikz@modcmd\tikz@decoration + \hobby@restore@tikz@modcmd\tikz@@close + \global\let\hobby@curveto@delegate=\hobby@qcurveto@auto +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qinit@tikz@modcmd} +% \begin{macrocode} +\def\hobby@qinit@tikz@modcmd#1{% + \expandafter\global\expandafter\let\csname hobby@orig@\string#1\endcsname=#1% + \gdef#1{\hobby@qfinish#1}% +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby@qfinish} +% \begin{macrocode} +\def\hobby@qfinish{% + \hobby@quick@computeend + \hobby@qrestore@tikz@commands +} +% \end{macrocode} +% \end{macro} + +% \iffalse +%</tikzlibrary> +% \fi +% +% \subsection{Arrays} +% +% \iffalse +%<*array> +% \fi +% +% +% A lot of our data structures are really arrays. +% These are implemented as \LaTeX3 ``property lists''. +% For ease of use, an array is a property list with numeric entries together with entries ``base'' and ``top'' which hold the lowest and highest indices that have been set. +% +% \begin{macrocode} +\RequirePackage{expl3} +\ExplSyntaxOn +% \end{macrocode} +% Some auxiliary variables. +% \begin{macrocode} +\tl_new:N \l_array_tmp_tl +\tl_new:N \l_array_show_tl +\int_new:N \l_array_base_int +\int_new:N \l_array_top_int +\int_new:N \l_array_tmp_int +\int_new:N \g_array_map_int +% \end{macrocode} +% The global variable \Verb+\g_array_base_int+ says what index a blank array should start with when pushed or unshifted. +% \begin{macrocode} +\int_new:N \g_array_base_int +\int_gset:Nn \g_array_base_int {0} +% \end{macrocode} +% \begin{macro}{\array_adjust_ends:Nn} +% This ensures that the ``base'' and ``top'' are big enough to include the given index. +% \begin{macrocode} +\cs_new:Npn \array_adjust_ends:Nn #1#2 { + \prop_get:NnNTF #1 {base} \l_tmpa_tl + { + \int_compare:nNnTF {\l_tmpa_tl} > {#2} + { + \prop_put:Nnx #1 {base} {\int_eval:n {#2}} + } + {} + } + { + \prop_put:Nnx #1 {base} {\int_eval:n {#2}} + } + \prop_get:NnNTF #1 {top} \l_tmpa_tl + { + \int_compare:nNnTF {\l_tmpa_tl} < {#2} + { + \prop_put:Nnx #1 {top} {\int_eval:n {#2}} + } + {} + } + { + \prop_put:Nnx #1 {top} {\int_eval:n {#2}} + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gadjust_ends:Nn} +% This ensures that the ``base'' and ``top'' are big enough to include the given index. +% (Global version) +% \begin{macrocode} +\cs_new:Npn \array_gadjust_ends:Nn #1#2 { + \prop_get:NnNTF #1 {base} \l_tmpa_tl + { + \int_compare:nNnTF {\l_tmpa_tl} > {#2} + { + \prop_gput:Nnx #1 {base} {\int_eval:n {#2}} + } + {} + } + { + \prop_gput:Nnx #1 {base} {\int_eval:n {#2}} + } + \prop_get:NnNTF #1 {top} \l_tmpa_tl + { + \int_compare:nNnTF {\l_tmpa_tl} < {#2} + { + \prop_gput:Nnx #1 {top} {\int_eval:n {#2}} + } + {} + } + { + \prop_gput:Nnx #1 {top} {\int_eval:n {#2}} + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_put:Nnn} +% When adding a value to an array we have to adjust the ends. +% \begin{macrocode} +\cs_new:Npn \array_put:Nnn #1#2#3 { + \exp_args:NNx \prop_put:Nnn #1 {\int_eval:n {#2}} {#3} + \array_adjust_ends:Nn #1{#2} +} +\cs_generate_variant:Nn \array_put:Nnn {Nnx} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gput:Nnn} +% When adding a value to an array we have to adjust the ends. +% (Global version) +% \begin{macrocode} +\cs_new:Npn \array_gput:Nnn #1#2#3 { + \exp_args:NNx \prop_gput:Nnn #1 {\int_eval:n {#2}} {#3} + \array_gadjust_ends:Nn #1{#2} +} +\cs_generate_variant:Nn \array_gput:Nnn {Nnx} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_get:NnN} +% \begin{macrocode} +\cs_new:Npn \array_get:NnN #1#2#3 { + \exp_args:NNx \prop_get:NnN #1 {\int_eval:n {#2}} #3 +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}[EXP]{\array_get:Nn} +% \begin{macrocode} +\cs_new:Npn \array_get:Nn #1#2 { + \exp_args:NNf \prop_item:Nn #1 { \int_eval:n {#2} } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_get:NnNTF} +% \begin{macrocode} +\cs_new:Npn \array_get:NnNTF #1#2#3#4#5 { + \exp_args:NNx \prop_get:NnNTF #1 {\int_eval:n {#2}} #3 {#4}{#5} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_if_empty:NTF} +% \begin{macrocode} +\prg_new_conditional:Npnn \array_if_empty:N #1 { p, T, F, TF } +{ + \if_meaning:w #1 \c_empty_prop + \prg_return_true: + \else: + \prg_return_false: + \fi: +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_if_exist:NTF} +% \begin{macrocode} +\prg_new_eq_conditional:NNn \array_if_exist:N \cs_if_exist:N { p, T, F, TF } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_new:N} +% \begin{macrocode} +\cs_new_eq:NN \array_new:N \prop_new:N +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_clear:N} +% \begin{macrocode} +\cs_new_eq:NN \array_clear:N \prop_clear:N +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gclear:N} +% \begin{macrocode} +\cs_new_eq:NN \array_gclear:N \prop_gclear:N +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_map_function} +% When stepping through an array, we want to iterate in order so a simple wrapper to \Verb+\prop_map_function+ is not enough. +% This maps through every value from the base to the top so the function should be prepared to deal with a \Verb+\q_no_value+. +% \begin{macrocode} +\cs_new:Npn \array_map_function:NN #1#2 +{ + \array_if_empty:NTF #1 {} { + \prop_get:NnNTF #1 {base} \l_array_tmp_tl { + \int_set:Nn \l_array_base_int {\l_array_tmp_tl} + }{ + \int_set:Nn \l_array_base_int {0} + } + \prop_get:NnNTF #1 {top} \l_array_tmp_tl { + \int_set:Nn \l_array_top_int {\l_array_tmp_tl} + }{ + \int_set:Nn \l_array_top_int {0} + } + \int_step_inline:nnnn {\l_array_base_int} {1} {\l_array_top_int} { + \array_get:NnN #1 {##1} \l_array_tmp_tl + \exp_args:NnV #2 {##1} \l_array_tmp_tl +} +} {} +} +\cs_generate_variant:Nn \array_map_function:NN { Nc } +\cs_generate_variant:Nn \array_map_function:NN { c , cc } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_reverse_map_function} +% This steps through the array in reverse order. +% \begin{macrocode} +\cs_new:Npn \array_reverse_map_function:NN #1#2 +{ + \array_if_empty:NTF #1 {} { + \prop_get:NnNTF #1 {base} \l_array_tmp_tl { + \int_set:Nn \l_array_base_int {\l_array_tmp_tl} + }{ + \int_set:Nn \l_array_base_int {0} + } + \prop_get:NnNTF #1 {top} \l_array_tmp_tl { + \int_set:Nn \l_array_top_int {\l_array_tmp_tl} + }{ + \int_set:Nn \l_array_top_int {0} + } + \int_step_inline:nnnn {\l_array_top_int} {-1} {\l_array_base_int} { + \array_get:NnN #1 {##1} \l_array_tmp_tl + \exp_args:Nno #2 {##1} \l_array_tmp_tl +} +} {} +} +\cs_generate_variant:Nn \array_reverse_map_function:NN { Nc } +\cs_generate_variant:Nn \array_reverse_map_function:NN { c , cc } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_map_inline:Nn} +% Inline version of the above. +% \begin{macrocode} +\cs_new_protected:Npn \array_map_inline:Nn #1#2 + { + \int_gincr:N \g_array_map_int + \cs_gset:cpn { array_map_inline_ \int_use:N \g_array_map_int :nn } + ##1##2 {#2} + \exp_args:NNc \array_map_function:NN #1 + { array_map_inline_ \int_use:N \g_array_map_int :nn } + \prg_break_point:Nn \array_map_break: { \int_gdecr:N \g_array_map_int } + } +\cs_generate_variant:Nn \array_map_inline:Nn { c } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_reverse_map_inline:Nn} +% Inline version of the above. +% \begin{macrocode} +\cs_new_protected:Npn \array_reverse_map_inline:Nn #1#2 + { + \int_gincr:N \g_array_map_int + \cs_gset:cpn { array_map_inline_ \int_use:N \g_array_map_int :nn } + ##1##2 {#2} + \exp_args:NNc \array_reverse_map_function:NN #1 + { array_map_inline_ \int_use:N \g_array_map_int :nn } + \prg_break_point:Nn \array_map_break: { \int_gdecr:N \g_array_map_int } + } +\cs_generate_variant:Nn \array_reverse_map_inline:Nn { c } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_map_break:} +% \begin{macrocode} +\cs_new_nopar:Npn \array_map_break: + { \prg_map_break:Nn \array_map_break: { } } +\cs_new_nopar:Npn \array_map_break:n + { \prg_map_break:Nn \array_map_break: } +% \end{macrocode} +% \end{macro} +% +% For displaying arrays, we need some messages. +% \begin{macrocode} +\msg_new:nnn { kernel } { show-array } + { + The~array~\token_to_str:N #1~ + \array_if_empty:NTF #1 + { is~empty } + { contains~the~items~(without~outer~braces): } + } +% \end{macrocode} +% +% \begin{macro}{\array_show:N} +% Mapping through an array isn't expandable so we have to set a token list to its contents first before passing it to the message handler. +% \begin{macrocode} +\cs_new_protected:Npn \array_show:N #1 + { + \__msg_show_variable:NNNnn + #1 + \array_if_exist:NTF + \array_if_empty:NTF + { array } + { \array_map_function:NN #1 \__msg_show_item:nn } + } +\cs_generate_variant:Nn \array_show:N { c } +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_push:Nn} +% \begin{macrocode} +\cs_new_protected:Npn \array_push:Nn #1#2 +{ + \prop_get:NnNTF #1 {top} \l_array_tmp_tl + { + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \int_incr:N \l_array_tmp_int + \array_put:Nnn #1 {\l_array_tmp_int} {#2} + } + { + \array_put:Nnn #1 {\g_array_base_int} {#2} + } +} +\cs_generate_variant:Nn \array_push:Nn {Nx} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gpush:Nn} +% \begin{macrocode} +\cs_new_protected:Npn \array_gpush:Nn #1#2 +{ + \prop_get:NnNTF #1 {top} \l_array_tmp_tl + { + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \int_incr:N \l_array_tmp_int + \array_gput:Nnn #1 {\l_array_tmp_int} {#2} + } + { + \array_gput:Nnn #1 {\g_array_base_int} {#2} + } +} +\cs_generate_variant:Nn \array_gpush:Nn {Nx} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_unshift:Nn} +% \begin{macrocode} +\cs_new_protected:Npn \array_unshift:Nn #1#2 +{ + \prop_get:NnNTF #1 {base} \l_array_tmp_tl + { + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \int_decr:N \l_array_tmp_int + \array_put:Nnn #1 {\l_array_tmp_int} {#2} + } + { + \array_put:Nnn #1 {\g_array_base_int} {#2} + } +} +\cs_generate_variant:Nn \array_unshift:Nn {Nx} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gunshift:Nn} +% \begin{macrocode} +\cs_new_protected:Npn \array_gunshift:Nn #1#2 +{ + \prop_get:NnNTF #1 {base} \l_array_tmp_tl + { + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \int_decr:N \l_array_tmp_int + \array_gput:Nnn #1 {\l_array_tmp_int} {#2} + } + { + \array_gput:Nnn #1 {\g_array_base_int} {#2} + } +} +\cs_generate_variant:Nn \array_gunshift:Nn {Nx} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_pop:NN} +% \begin{macrocode} +\cs_new_protected:Npn \array_pop:NN #1#2 +{ + \prop_get:NnN #1 {top} \l_array_tmp_tl + \array_get:NnN #1 {\l_array_tmp_tl} #2 + \array_del:Nn #1 {\l_array_tmp_tl} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gpop:NN} +% \begin{macrocode} +\cs_new_protected:Npn \array_gpop:NN #1#2 +{ + \prop_get:NnN #1 {top} \l_array_tmp_tl + \array_get:NnN #1 {\l_array_tmp_tl} #2 + \array_gdel:Nn #1 {\l_array_tmp_tl} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_shift:NN} +% \begin{macrocode} +\cs_new_protected:Npn \array_shift:NN #1#2 +{ + \prop_get:NnN #1 {base} \l_array_tmp_tl + \array_get:NnN #1 {\l_array_tmp_tl} #2 + \array_del:Nn #1 {\l_array_tmp_tl} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gshift:NN} +% \begin{macrocode} +\cs_new_protected:Npn \array_gshift:NN #1#2 +{ + \prop_get:NnN #1 {base} \l_array_tmp_tl + \array_get:NnN #1 {\l_array_tmp_tl} #2 + \array_gdel:Nn #1 {\l_array_tmp_tl} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_top:NN} +% \begin{macrocode} +\cs_new_protected:Npn \array_top:NN #1#2 +{ + \prop_get:NnN #1 {top} \l_array_tmp_tl + \array_get:NnN #1 {\l_array_tmp_tl} #2 +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_base:NN} +% \begin{macrocode} +\cs_new_protected:Npn \array_base:NN #1#2 +{ + \prop_get:NnN #1 {base} \l_array_tmp_tl + \array_get:NnN #1 {\l_array_tmp_tl} #2 +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_top:N} +% \begin{macrocode} +\cs_new:Npn \array_top:N #1 +{ + \array_get:Nn #1 {\prop_item:Nn #1 {top}} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_base:N} +% \begin{macrocode} +\cs_new:Npn \array_base:N #1 +{ + \array_get:Nn #1 {\prop_item:Nn #1 {base}} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_del:Nn} +% \begin{macrocode} +\cs_new_protected:Npn \array_del:Nn #1#2 +{ + \exp_args:NNx \prop_pop:Nn #1 {\int_eval:n {#2}} + \int_set:Nn \l_array_tmp_int {0} + \array_map_inline:Nn #1 { + \tl_if_eq:NNTF {##2} {\q_no_value} {} + { + \int_incr:N \l_array_tmp_int + } + } + \int_compare:nNnTF {\l_array_tmp_int} = {0} + { + \prop_clear:N #1 + } + { + \prop_get:NnN #1 {top} \l_array_tmp_tl + \int_compare:nNnTF {#2} = {\l_array_tmp_tl} { + \prop_get:NnN #1 {base} \l_array_tmp_tl + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \array_map_inline:Nn #1 { + \tl_if_eq:NNTF {##2} {\q_no_value} {} + { + \int_compare:nNnTF {\l_array_tmp_int} < {##1} { + \int_set:Nn \l_array_tmp_int {##1} + }{} + } + } + \prop_put:Nnx #1 {top} {\int_use:N \l_array_tmp_int} + }{} + \prop_get:NnN #1 {base} \l_array_tmp_tl + \int_compare:nNnTF {#2} = {\l_array_tmp_tl} { + \prop_get:NnN #1 {top} \l_array_tmp_tl + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \array_map_inline:Nn #1 { + \tl_if_eq:NNTF {##2} {\q_no_value} {} + { + \int_compare:nNnTF {\l_array_tmp_int} > {##1} { + \int_set:Nn \l_array_tmp_int {##1} + }{} + } + } + \prop_put:Nnx #1 {base} {\int_use:N \l_array_tmp_int} + }{} + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_gdel:Nn} +% \begin{macrocode} +\cs_new_protected:Npn \array_gdel:Nn #1#2 +{ + \exp_args:NNx \prop_gremove:Nn #1 {\int_eval:n {#2}} + \int_set:Nn \l_array_tmp_int {0} + \array_map_inline:Nn #1 { + \tl_if_eq:NNTF {##2} {\q_no_value} {} + { + \int_incr:N \l_array_tmp_int + } + } + \int_compare:nNnTF {\l_array_tmp_int} = {0} + { + \prop_gclear:N #1 + } + { + \prop_get:NnN #1 {top} \l_array_tmp_tl + \int_compare:nNnTF {#2} = {\l_array_tmp_tl} { + \prop_get:NnN #1 {base} \l_array_tmp_tl + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \array_map_inline:Nn #1 { + \tl_if_eq:NNTF {##2} {\q_no_value} {} + { + \int_compare:nNnTF {\l_array_tmp_int} < {##1} { + \int_set:Nn \l_array_tmp_int {##1} + }{} + } + } + \prop_gput:Nnx #1 {top} {\int_use:N \l_array_tmp_int} + }{} + \prop_get:NnN #1 {base} \l_array_tmp_tl + \int_compare:nNnTF {#2} = {\l_array_tmp_tl} { + \prop_get:NnN #1 {top} \l_array_tmp_tl + \int_set:Nn \l_array_tmp_int {\l_array_tmp_tl} + \array_map_inline:Nn #1 { + \tl_if_eq:NNTF {##2} {\q_no_value} {} + { + \int_compare:nNnTF {\l_array_tmp_int} > {##1} { + \int_set:Nn \l_array_tmp_int {##1} + }{} + } + } + \prop_gput:Nnx #1 {base} {\int_use:N \l_array_tmp_int} + }{} + } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\array_length:N} +% \begin{macrocode} +\cs_new_protected:Npn \array_length:N #1 +{ + \int_eval:n {\prop_item:Nn #1 {top} - \prop_item:Nn #1 {base}} +} +% \end{macrocode} +% \end{macro} +% \begin{macrocode} +\ExplSyntaxOff +% \end{macrocode} +% +% \iffalse +%</array> +% \fi +% +% \iffalse +%<*l3hobby> +% \fi +% +% \begin{macrocode} +\RequirePackage{expl3} +% \end{macrocode} +% +% Load the hobby core +% \begin{macrocode} +\input{hobby.code.tex} +% \end{macrocode} +% +% Register as an expl3 package +% \begin{macrocode} +\ProvidesExplPackage {hobby-l3draw} {2018/02/20} {1.0} {Interface for l3draw and hobby} +% \end{macrocode} +% +% Load the l3draw package +% \begin{macrocode} +\RequirePackage{l3draw} +% \end{macrocode} +% +% +% \begin{macro}{\hobby_draw_moveto:nnn} +% This provides our interface between hobby's moveto and l3draw's moveto +% \begin{macrocode} +\cs_new_protected:Npn \hobby_draw_moveto:nnn #1#2#3 +{ + \draw_path_canvas_moveto:n {#3} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_draw_curveto:nnn} +% This provides our interface between hobby's curveto and l3draw's curveto +% \begin{macrocode} +\cs_set_eq:NN \hobby_draw_curveto:nnn \draw_path_canvas_curveto:nnn +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_draw_close:n} +% This provides our interface between hobby's close and l3draw's close +% \begin{macrocode} +\cs_new_protected:Npn \hobby_draw_close:n #1 +{ + \draw_path_close: +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_draw_addpoint:n} +% This processes a point and passes it one more step towards the hobby algorithm +% \begin{macrocode} +\cs_new_protected:Npn \hobby_draw_addpoint:n #1 +{ + \__draw_point_process:nn + { \__hobby_draw_addpoint:nn } + { \draw_point_transform:n {#1} } +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\__hobby_draw_curveto:nn} +% This provides our interface between l3draw's points and hobby's syntax +% \begin{macrocode} +\cs_new_protected:Npn \__hobby_draw_addpoint:nn #1#2 +{ + \hobby_add_point:nnnnnn {#1} {#2} {1} {1} {0} {10} +} +% \end{macrocode} +% \end{macro} +% +% \begin{macro}{\hobby_draw_init:} +% This initialises hobby's algorithm with the l3draw commands +% \begin{macrocode} +\cs_new_protected:Npn \hobby_draw_init: +{ + \hobby_set_cmds:NNN \hobby_draw_moveto:nnn \hobby_draw_curveto:nnn \hobby_draw_close:n + \hobby_clear_path: +} +% \end{macrocode} +% \end{macro} +% +% \iffalse +%</l3hobby> +% \fi +%\Finale |