% \iffalse meta-comment % %% File: l3draw-softpath.dtx % % Copyright (C) 2018-2024 The LaTeX Project % % It may be distributed and/or modified under the conditions of the % LaTeX Project Public License (LPPL), either version 1.3c of this % license or (at your option) any later version. The latest version % of this license is in the file % % http://www.latex-project.org/lppl.txt % % This file is part of the "l3experimental bundle" (The Work in LPPL) % and all files in that bundle must be distributed together. % % ----------------------------------------------------------------------- % % The development version of the bundle can be found at % % https://github.com/latex3/latex3 % % for those people who are interested. % %<*driver> \RequirePackage{expl3} \documentclass[full]{l3doc} \begin{document} \DocInput{\jobname.dtx} \end{document} % % \fi % % \title{^^A % The \pkg{l3draw-softpath} package\\ Soft paths^^A % } % % \author{^^A % The \LaTeX{} Project\thanks % {^^A % E-mail: % \href{mailto:latex-team@latex-project.org} % {latex-team@latex-project.org}^^A % }^^A % } % % \date{Released 2024-03-14} % % \maketitle % % \begin{implementation} % % \section{\pkg{l3draw-softpath} implementation} % % \begin{macrocode} %<*package> % \end{macrocode} % % \begin{macrocode} %<@@=draw> % \end{macrocode} % % \subsection{Managing soft paths} % % There are two linked aims in the code here. The most significant is to % provide a way to modify paths, for example to shorten the ends or round % the corners. This means that the path cannot be written piecemeal as % specials, but rather needs to be held in macros. The second aspect that % follows from this is performance: simply adding to a single macro a piece % at a time will have poor performance as the list gets long so we use % \cs[no-index]{tl_build_\ldots{}} functions. % % Each marker (operation) token takes two arguments, which makes processing % more straight-forward. As such, some operations have dummy arguments, whilst % others have to be split over several tokens. As the code here is at a low % level, all dimension arguments are assumed to be explicit and fully-expanded. % % \begin{variable}{\g_@@_softpath_main_tl} % The soft path itself. % \begin{macrocode} \tl_new:N \g_@@_softpath_main_tl % \end{macrocode} % \end{variable} % % \begin{variable}{\l_@@_softpath_tmp_tl} % Scratch space. % \begin{macrocode} \tl_new:N \l_@@_softpath_tmp_tl % \end{macrocode} % \end{variable} % % \begin{variable}{\g_@@_softpath_corners_bool} % Allow for optimised path use. % \begin{macrocode} \bool_new:N \g_@@_softpath_corners_bool % \end{macrocode} % \end{variable} % % \begin{macro}{\@@_softpath_add:n, \@@_softpath_add:o, \@@_softpath_add:e} % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_add:n { \tl_build_gput_right:Nn \g_@@_softpath_main_tl } \cs_generate_variant:Nn \@@_softpath_add:n { o, e } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_softpath_use:, \@@_softpath_clear:} % Using and clearing is trivial. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_use: { \tl_build_get_intermediate:NN \g_@@_softpath_main_tl \l_@@_softpath_tmp_tl \l_@@_softpath_tmp_tl } \cs_new_protected:Npn \@@_softpath_clear: { \tl_build_gbegin:N \g_@@_softpath_main_tl \bool_gset_false:N \g_@@_softpath_corners_bool } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_softpath_save:, \@@_softpath_restore:} % Abstracted ideas to keep variables inside this submodule. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_save: { \tl_build_gend:N \g_@@_softpath_main_tl \tl_set_eq:NN \l_@@_softpath_main_tl \g_@@_softpath_main_tl \bool_set_eq:NN \l_@@_softpath_corners_bool \g_@@_softpath_corners_bool \@@_softpath_clear: } \cs_new_protected:Npn \@@_softpath_restore: { \@@_softpath_clear: \@@_softpath_add:o \l_@@_softpath_main_tl \bool_gset_eq:NN \g_@@_softpath_corners_bool \l_@@_softpath_corners_bool } % \end{macrocode} % \end{macro} % % \begin{variable}{\g_@@_softpath_lastx_dim, \g_@@_softpath_lasty_dim} % For tracking the end of the path (to close it). % \begin{macrocode} \dim_new:N \g_@@_softpath_lastx_dim \dim_new:N \g_@@_softpath_lasty_dim % \end{macrocode} % \end{variable} % % \begin{variable}{\g_@@_softpath_move_bool} % Track if moving a point should update the close position. % \begin{macrocode} \bool_new:N \g_@@_softpath_move_bool \bool_gset_true:N \g_@@_softpath_move_bool % \end{macrocode} % \end{variable} % % \begin{macro}{\@@_softpath_closepath:} % \begin{macro}{\@@_softpath_curveto:nnnnnn} % \begin{macro} % { % \@@_softpath_lineto:nn, % \@@_softpath_moveto:nn % } % \begin{macro}{\@@_softpath_rectangle:nnnn} % \begin{macro}{\@@_softpath_roundpoint:nn, \@@_softpath_roundpoint:VV} % The various parts of a path expressed as the appropriate soft path % functions. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_closepath: { \@@_softpath_add:e { \@@_softpath_close_op:nn { \dim_use:N \g_@@_softpath_lastx_dim } { \dim_use:N \g_@@_softpath_lasty_dim } } } \cs_new_protected:Npn \@@_softpath_curveto:nnnnnn #1#2#3#4#5#6 { \@@_softpath_add:n { \@@_softpath_curveto_opi:nn {#1} {#2} \@@_softpath_curveto_opii:nn {#3} {#4} \@@_softpath_curveto_opiii:nn {#5} {#6} } } \cs_new_protected:Npn \@@_softpath_lineto:nn #1#2 { \@@_softpath_add:n { \@@_softpath_lineto_op:nn {#1} {#2} } } \cs_new_protected:Npn \@@_softpath_moveto:nn #1#2 { \@@_softpath_add:n { \@@_softpath_moveto_op:nn {#1} {#2} } \bool_if:NT \g_@@_softpath_move_bool { \dim_gset:Nn \g_@@_softpath_lastx_dim {#1} \dim_gset:Nn \g_@@_softpath_lasty_dim {#2} } } \cs_new_protected:Npn \@@_softpath_rectangle:nnnn #1#2#3#4 { \@@_softpath_add:n { \@@_softpath_rectangle_opi:nn {#1} {#2} \@@_softpath_rectangle_opii:nn {#3} {#4} } } \cs_new_protected:Npn \@@_softpath_roundpoint:nn #1#2 { \@@_softpath_add:n { \@@_softpath_roundpoint_op:nn {#1} {#2} } \bool_gset_true:N \g_@@_softpath_corners_bool } \cs_generate_variant:Nn \@@_softpath_roundpoint:nn { VV } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % % \begin{macro} % { % \@@_softpath_close_op:nn , % \@@_softpath_curveto_opi:nn , % \@@_softpath_curveto_opii:nn , % \@@_softpath_curveto_opiii:nn , % \@@_softpath_lineto_op:nn , % \@@_softpath_moveto_op:nn , % \@@_softpath_roundpoint_op:nn , % \@@_softpath_rectangle_opi:nn , % \@@_softpath_rectangle_opii:nn % } % \begin{macro}{\@@_softpath_curveto_opi:nnNnnNnn} % \begin{macro}{\@@_softpath_rectangle_opi:nnNnn} % The markers for operations: all the top-level ones take two arguments. % The support tokens for curves have to be different in meaning to a % round point, hence being quark-like. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_close_op:nn #1#2 { \@@_backend_closepath: } \cs_new_protected:Npn \@@_softpath_curveto_opi:nn #1#2 { \@@_softpath_curveto_opi:nnNnnNnn {#1} {#2} } \cs_new_protected:Npn \@@_softpath_curveto_opi:nnNnnNnn #1#2#3#4#5#6#7#8 { \@@_backend_curveto:nnnnnn {#1} {#2} {#4} {#5} {#7} {#8} } \cs_new_protected:Npn \@@_softpath_curveto_opii:nn #1#2 { \@@_softpath_curveto_opii:nn } \cs_new_protected:Npn \@@_softpath_curveto_opiii:nn #1#2 { \@@_softpath_curveto_opiii:nn } \cs_new_protected:Npn \@@_softpath_lineto_op:nn #1#2 { \@@_backend_lineto:nn {#1} {#2} } \cs_new_protected:Npn \@@_softpath_moveto_op:nn #1#2 { \@@_backend_moveto:nn {#1} {#2} } \cs_new_protected:Npn \@@_softpath_roundpoint_op:nn #1#2 { \@@_softpath_roundpoint_op:nn } \cs_new_protected:Npn \@@_softpath_rectangle_opi:nn #1#2 { \@@_softpath_rectangle_opi:nnNnn {#1} {#2} } \cs_new_protected:Npn \@@_softpath_rectangle_opi:nnNnn #1#2#3#4#5 { \@@_backend_rectangle:nnnn {#1} {#2} {#4} {#5} } \cs_new_protected:Npn \@@_softpath_rectangle_opii:nn #1#2 { \@@_softpath_rectangle_opii:nn } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % % \subsection{Rounding soft path corners} % % The aim here is to find corner rounding points and to replace them with % arcs of appropriate length. The approach is exactly that in \pkg{pgf}: % step through, find the corners, find the supporting data, do the rounding. % % \begin{variable}{\l_@@_softpath_main_tl} % For constructing the updated path. % \begin{macrocode} \tl_new:N \l_@@_softpath_main_tl % \end{macrocode} % \end{variable} % % \begin{variable}{\l_@@_softpath_part_tl} % Data structures. % \begin{macrocode} \tl_new:N \l_@@_softpath_part_tl \tl_new:N \l_@@_softpath_curve_end_tl % \end{macrocode} % \end{variable} % % \begin{variable} % {\l_@@_softpath_lastx_fp, \l_@@_softpath_lasty_fp} % \begin{variable} % {\l_@@_softpath_corneri_dim, \l_@@_softpath_cornerii_dim} % \begin{variable}{\l_@@_softpath_first_tl, \l_@@_softpath_move_tl} % Position tracking: the token list data may be entirely empty or set to % a co-ordinate. % \begin{macrocode} \fp_new:N \l_@@_softpath_lastx_fp \fp_new:N \l_@@_softpath_lasty_fp \dim_new:N \l_@@_softpath_corneri_dim \dim_new:N \l_@@_softpath_cornerii_dim \tl_new:N \l_@@_softpath_first_tl \tl_new:N \l_@@_softpath_move_tl % \end{macrocode} % \end{variable} % \end{variable} % \end{variable} % % \begin{variable}{\c_@@_softpath_arc_fp} % The magic constant. % \begin{macrocode} \fp_const:Nn \c_@@_softpath_arc_fp { 4/3 * (sqrt(2) - 1) } % \end{macrocode} % \end{variable} % % \begin{macro}{\@@_softpath_round_corners:} % \begin{macro}{\@@_softpath_round_loop:Nnn} % \begin{macro}{\@@_softpath_round_action:nn} % \begin{macro}{\@@_softpath_round_action:Nnn} % \begin{macro}{\@@_softpath_round_action_curveto:NnnNnn} % \begin{macro}{\@@_softpath_round_action_close:} % \begin{macro}{\@@_softpath_round_lookahead:NnnNnn} % \begin{macro}{\@@_softpath_round_roundpoint:NnnNnnNnn} % \begin{macro}{\@@_softpath_round_calc:NnnNnn} % \begin{macro}[EXP] % {\@@_softpath_round_calc:nnnnnn, \@@_softpath_round_calc:eVnnnn} % \begin{macro}[EXP]{\@@_softpath_round_calc:nnnnw} % \begin{macro}{\@@_softpath_round_close:nn} % \begin{macro}[EXP]{\@@_softpath_round_close:w} % \begin{macro}{\@@_softpath_round_end:} % Rounding corners on a path means going through the entire path and % adjusting it. As such, we avoid this entirely if we know there are no % corners to deal with. Assuming there is work to do, we recover the existing % path and start a loop. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_corners: { \bool_if:NT \g_@@_softpath_corners_bool { \group_begin: \tl_clear:N \l_@@_softpath_main_tl \tl_clear:N \l_@@_softpath_part_tl \fp_zero:N \l_@@_softpath_lastx_fp \fp_zero:N \l_@@_softpath_lasty_fp \tl_clear:N \l_@@_softpath_first_tl \tl_clear:N \l_@@_softpath_move_tl \tl_build_gend:N \g_@@_softpath_main_tl \exp_after:wN \@@_softpath_round_loop:Nnn \g_@@_softpath_main_tl \q_@@_recursion_tail ? ? \q_@@_recursion_stop \group_end: } \bool_gset_false:N \g_@@_softpath_corners_bool } % \end{macrocode} % The loop can take advantage of the fact that all soft path operations are % made up of a token followed by two arguments. At this stage, there is % a simple split: have we round a round point. If so, is there any actual % rounding to be done: if the arcs have come through zero, just ignore it. % In cases where we are not at a corner, we simply move along the path, % allowing for any new part starting due to a \texttt{moveto}. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_loop:Nnn #1#2#3 { \@@_if_recursion_tail_stop_do:Nn #1 { \@@_softpath_round_end: } \token_if_eq_meaning:NNTF #1 \@@_softpath_roundpoint_op:nn { \@@_softpath_round_action:nn {#2} {#3} } { \tl_if_empty:NT \l_@@_softpath_first_tl { \tl_set:Nn \l_@@_softpath_first_tl { {#2} {#3} } } \fp_set:Nn \l_@@_softpath_lastx_fp {#2} \fp_set:Nn \l_@@_softpath_lasty_fp {#3} \token_if_eq_meaning:NNTF #1 \@@_softpath_moveto_op:nn { \tl_put_right:No \l_@@_softpath_main_tl \l_@@_softpath_move_tl \tl_put_right:No \l_@@_softpath_main_tl \l_@@_softpath_part_tl \tl_set:Nn \l_@@_softpath_move_tl { #1 {#2} {#3} } \tl_clear:N \l_@@_softpath_first_tl \tl_clear:N \l_@@_softpath_part_tl } { \tl_put_right:Nn \l_@@_softpath_part_tl { #1 {#2} {#3} } } \@@_softpath_round_loop:Nnn } } \cs_new_protected:Npn \@@_softpath_round_action:nn #1#2 { \dim_set:Nn \l_@@_softpath_corneri_dim {#1} \dim_set:Nn \l_@@_softpath_cornerii_dim {#2} \bool_lazy_and:nnTF { \dim_compare_p:nNn \l_@@_softpath_corneri_dim = { 0pt } } { \dim_compare_p:nNn \l_@@_softpath_cornerii_dim = { 0pt } } { \@@_softpath_round_loop:Nnn } { \@@_softpath_round_action:Nnn } } % \end{macrocode} % We now have a round point to work on and have grabbed the next item in % the path. There are only a few cases where we have to do anything. Each of % them is picked up by looking for the appropriate action. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_action:Nnn #1#2#3 { \tl_if_empty:NT \l_@@_softpath_first_tl { \tl_set:Nn \l_@@_softpath_first_tl { {#2} {#3} } } \token_if_eq_meaning:NNTF #1 \@@_softpath_curveto_opi:nn { \@@_softpath_round_action_curveto:NnnNnn } { \token_if_eq_meaning:NNTF #1 \@@_softpath_close_op:nn { \@@_softpath_round_action_close: } { \token_if_eq_meaning:NNTF #1 \@@_softpath_lineto_op:nn { \@@_softpath_round_lookahead:NnnNnn } { \@@_softpath_round_loop:Nnn } } } #1 {#2} {#3} } % \end{macrocode} % For a curve, we collect the two control points then move on to grab the % end point and add the curve there: the second control point becomes our % starter. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_action_curveto:NnnNnn #1#2#3#4#5#6 { \tl_put_right:Nn \l_@@_softpath_part_tl { #1 {#2} {#3} #4 {#5} {#6} } \fp_set:Nn \l_@@_softpath_lastx_fp {#5} \fp_set:Nn \l_@@_softpath_lasty_fp {#6} \@@_softpath_round_lookahead:NnnNnn } \cs_new_protected:Npn \@@_softpath_round_action_close: { \bool_lazy_and:nnTF { ! \tl_if_empty_p:N \l_@@_softpath_first_tl } { ! \tl_if_empty_p:N \l_@@_softpath_move_tl } { \exp_after:wN \@@_softpath_round_close:nn \l_@@_softpath_first_tl } { \@@_softpath_round_loop:Nnn } } % \end{macrocode} % At this stage we have a current (sub)operation (|#1|) and the next % operation (|#4|), and can therefore decide whether to round or not. % In the case of yet another rounding marker, we have to look a bit % further ahead. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_lookahead:NnnNnn #1#2#3#4#5#6 { \bool_lazy_any:nTF { { \token_if_eq_meaning_p:NN #4 \@@_softpath_lineto_op:nn } { \token_if_eq_meaning_p:NN #4 \@@_softpath_curveto_opi:nn } { \token_if_eq_meaning_p:NN #4 \@@_softpath_close_op:nn } } { \@@_softpath_round_calc:NnnNnn \@@_softpath_round_loop:Nnn {#5} {#6} } { \token_if_eq_meaning:NNTF #4 \@@_softpath_roundpoint_op:nn { \@@_softpath_round_roundpoint:NnnNnnNnn } { \@@_softpath_round_loop:Nnn } } #1 {#2} {#3} #4 {#5} {#6} } \cs_new_protected:Npn \@@_softpath_round_roundpoint:NnnNnnNnn #1#2#3#4#5#6#7#8#9 { \@@_softpath_round_calc:NnnNnn \@@_softpath_round_loop:Nnn {#8} {#9} #1 {#2} {#3} #4 {#5} {#6} #7 {#8} {#9} } % \end{macrocode} % We now have all of the data needed to construct a rounded corner: all that % is left to do is to work out the detail! At this stage, we have details % of where the corner itself is (|#5|, |#6|), and where the next point is % (|#2|, |#3|). There are two types of calculations to do. First, we % need to interpolate from those two points in the direction of the % corner, in order to work out where the curve we are adding will start % and end. From those, plus the points we already have, we work out where % the control points will lie. All of this is done in an expansion to % avoid multiple calls to |\tl_put_right:Ne|. The end point of the line % is worked out up-front and saved: we need that if dealing with a % close-path operation. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_calc:NnnNnn #1#2#3#4#5#6 { \tl_set:Ne \l_@@_softpath_curve_end_tl { \draw_point_interpolate_distance:nnn \l_@@_softpath_cornerii_dim { #5 , #6 } { #2 , #3 } } \tl_put_right:Ne \l_@@_softpath_part_tl { \exp_not:N #4 \@@_softpath_round_calc:eVnnnn { \draw_point_interpolate_distance:nnn \l_@@_softpath_corneri_dim { #5 , #6 } { \l_@@_softpath_lastx_fp , \l_@@_softpath_lasty_fp } } \l_@@_softpath_curve_end_tl {#5} {#6} {#2} {#3} } \fp_set:Nn \l_@@_softpath_lastx_fp {#5} \fp_set:Nn \l_@@_softpath_lasty_fp {#6} #1 } % \end{macrocode} % At this stage we have the two curve end points, but they are in % co-ordinate form. So we split them up (with some more reordering). % \begin{macrocode} \cs_new:Npn \@@_softpath_round_calc:nnnnnn #1#2#3#4#5#6 { \@@_softpath_round_calc:nnnnw {#3} {#4} {#5} {#6} #1 \s_@@_mark #2 \s_@@_stop } \cs_generate_variant:Nn \@@_softpath_round_calc:nnnnnn { eV } % \end{macrocode} % The calculations themselves are relatively straight-forward, as we use a % quadratic Bézier curve. % \begin{macrocode} \cs_new:Npn \@@_softpath_round_calc:nnnnw #1#2#3#4 #5 , #6 \s_@@_mark #7 , #8 \s_@@_stop { {#5} {#6} \exp_not:N \@@_softpath_curveto_opi:nn { \fp_to_dim:n { #5 + \c_@@_softpath_arc_fp * ( #1 - #5 ) } } { \fp_to_dim:n { #6 + \c_@@_softpath_arc_fp * ( #2 - #6 ) } } \exp_not:N \@@_softpath_curveto_opii:nn { \fp_to_dim:n { #7 + \c_@@_softpath_arc_fp * ( #1 - #7 ) } } { \fp_to_dim:n { #8 + \c_@@_softpath_arc_fp* ( #2 - #8 ) } } \exp_not:N \@@_softpath_curveto_opiii:nn {#7} {#8} } % \end{macrocode} % To deal with a close-path operation, we need to do some manipulation. % It needs to be treated as a line operation for rounding, and then % have the close path operation re-added at the point where the curve % ends. That means saving the end point in the calculation step (see % earlier), and shuffling a lot. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_close:nn #1#2 { \use:e { \@@_softpath_round_calc:NnnNnn { \tl_set:Ne \exp_not:N \l_@@_softpath_move_tl { \@@_softpath_moveto_op:nn \exp_not:N \exp_after:wN \exp_not:N \@@_softpath_round_close:w \exp_not:N \l_@@_softpath_curve_end_tl \s_@@_stop } \use:e { \exp_not:N \exp_not:N \exp_not:N \use_i:nnnn { \@@_softpath_round_loop:Nnn \@@_softpath_close_op:nn \exp_not:N \exp_after:wN \exp_not:N \@@_softpath_round_close:w \exp_not:N \l_@@_softpath_curve_end_tl \s_@@_stop } } } {#1} {#2} \@@_softpath_lineto_op:nn \exp_after:wN \use_none:n \l_@@_softpath_move_tl } } \cs_new:Npn \@@_softpath_round_close:w #1 , #2 \s_@@_stop { {#1} {#2} } % \end{macrocode} % Tidy up the parts of the path, complete the built token list and put % it back into action. % \begin{macrocode} \cs_new_protected:Npn \@@_softpath_round_end: { \tl_put_right:No \l_@@_softpath_main_tl \l_@@_softpath_move_tl \tl_put_right:No \l_@@_softpath_main_tl \l_@@_softpath_part_tl \tl_build_gbegin:N \g_@@_softpath_main_tl \@@_softpath_add:o \l_@@_softpath_main_tl } % \end{macrocode} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % \end{macro} % % \begin{macrocode} % % \end{macrocode} % % \end{implementation} % % \PrintIndex