% \iffalse meta-comment %% ============================================================================= %% %% string-diagrams 0.3.0 (2026/11/07) %% %% Copyright (C) 2023 by Paolo Brasolin %% SPDX-License-Identifier: LPPL-1.3c %% %% ============================================================================= %% %% This work may be distributed and/or modified under the %% conditions of the LaTeX Project Public License, either version 1.3c %% of this license or (at your option) any later version. %% The latest version of this license is in %% https://www.latex-project.org/lppl.txt %% and version 1.3c or later is part of all distributions of LaTeX %% version 2008 or later. %% %% This work has the LPPL maintenance status `author-maintained'. %% %% The Current Maintainer of this work is Paolo Brasolin. %% %% This work consists of the files README.md, %% string-diagrams.dtx, %% string-diagrams.ins, %% and the derived files string-diagrams.sty, and %% string-diagrams.pdf. %% %% ============================================================================= %% % % %%%=[ DRIVER ]================================================================ % %<*driver> \documentclass[a4paper,full]{l3doc} \usepackage{parskip} \NewDocumentCommand{\TikZ}{}{Ti\emph{k}Z} \EnableCrossrefs \CodelineIndex \RecordChanges \usepackage{string-diagrams} \usetikzlibrary{calc} \usepackage{tcolorbox} \tcbuselibrary{listings, skins} \lstset{ language=[LaTeX]TeX, basicstyle=\MacroFont, columns=flexible, ^^A keywordstyle=\color{red}, ^^A morekeywords={}, texcsstyle=*\color{violet}, moretexcs={node,wires}, breaklines=true, } \tcbset{example/.style={ listing engine=listings, verbatim ignore percent=true, listing side text, size=minimal, skin=bicolor, colback=black!5!white, colbacklower=white, sidebyside, lefthand ratio=0.62, listing options={ xleftmargin=-1.6em, % poor man's gobble=4 }, }} \begin{document} \DocInput{\jobname.dtx} \PrintChanges \PrintIndex \end{document} % % % \fi % % \changes{0.1.0}{2023/05/31}{initial version} % \changes{0.2.0}{2023/06/12}{make box ports configurable} % \changes{0.3.0}{2026/11/07}{replace loose ends with wires to the boundary node \texttt{@}} % % \GetFileInfo{\jobname.sty} % % \title{^^A % The \pkg{\jobname} package^^A % \thanks{Thanks!}\\^^A % \fileinfo^^A % } % % \author{^^A % Paolo Brasolin\\^^A % \texttt{\href{mailto:paolo.brasolin@gmail.com}{paolo.brasolin@gmail.com}}^^A % } % % \date{\fileversion~(\filedate)} % % \maketitle % % ^^A=[ DOCUMENTATION ]========================================================= % % \begin{documentation} % % \begin{tcolorbox}[ % colback=red!5!white, % colframe=red, % sharp corners, % boxrule=1pt, % ] % Please note this is the \href{https://semver.org/#spec-item-4}{major version zero}, meant for initial development: \emph{anything MAY change at any time}. % The upside is that this is the best time to \href{https://github.com/paolobrasolin/string-diagrams#contributing}{contribute}! % Of course you can also just keep the \texttt{sty} along with your code and not care at all. % \end{tcolorbox} % % % \section{Documentation} % % \begin{function}[added=2023-05-31,updated=2026-11-07]{/pgf/box} % % To draw boxes, you use this style on a node. % Its label is already typeset in math mode, so write it without dollars: \texttt{\{f\}}, as \texttt{\{\$f\$\}} breaks the node. % % \begin{tcblisting}{example} % \begin{tikzpicture} % \node[box] {A}; % \end{tikzpicture} % \end{tcblisting} % % You can draw multiple boxes using any of your standard \TikZ\ positioning techniques. % Don't forget to label the nodes so you can easily reference them. % The last box below is placed with the \texttt{calc} library of \TikZ, which you load with \cs{usetikzlibrary}\texttt{\{calc\}}. % % \begin{tcblisting}{example} % \begin{tikzpicture} % \node[box] (A) at (0,0) {A}; % \node[box, right of=A] (B) {B}; % \node[box] (C) at ($(B)+(2cm,1em)$) {C}; % \end{tikzpicture} % \end{tcblisting} % \end{function} % % \begin{function}[added=2023-06-12,updated=2026-11-07]{ % /pgf/box ports north, % /pgf/box ports east, % /pgf/box ports south, % /pgf/box ports west, % } % \begin{syntax} % /pgf/box ports north=\meta{integer} % /pgf/box ports east=\meta{integer} % /pgf/box ports south=\meta{integer} % /pgf/box ports west=\meta{integer} % \end{syntax} % % You can open up any number of ports on any side of a box using the appropriate key; boxes have none until you do. % Like other \TikZ\ keys, these can also be set on a picture or a scope, for all the boxes in it. % Then, you can refer to the opened ports by their index: they are numbered from left to right on the north and south sides, and from top to bottom on the east and west sides. % Asking a box for a port it doesn't have is an error. % % \begin{tcblisting}{example,lefthand ratio=0.8} % \begin{tikzpicture}[ % marker/.style={circle, fill, inner sep=1pt, text=white}, % ] % % \node[ % box, % box ports north=3, % box ports east=3, % box ports south=3, % box ports west=3, % minimum width=6em, % minimum height=6em, % ] (A) {A}; % % \foreach \side in {north,east,south,west} % \foreach \index in {1,...,3} % \node[marker] at (A.\side.\index) {\index}; % % \end{tikzpicture} % \end{tcblisting} % % \end{function} % % \begin{function}[added=2023-06-12,updated=2026-11-07]{ % /pgf/box ports, % } % \begin{syntax} % /pgf/box ports=\meta{integer}/\meta{integer}/\meta{integer}/\meta{integer} % \end{syntax} % % The \texttt{box ports} key is a shortcut to set the number of ports on all sides at once. % It takes exactly four counts, for the north, east, south and west sides. % % \begin{tcblisting}{example,lefthand ratio=0.8} % \begin{tikzpicture}[ % marker/.style={circle, fill, inner sep=1pt}, % ] % % \node[box, box ports=1/2/3/4] (A) {A}; % % \foreach \side/\n in {north/1,east/2,south/3,west/4} % \foreach \index in {1,...,\n} % \node[marker] at (A.\side.\index) {}; % % \end{tikzpicture} % \end{tcblisting} % % The same value can also be passed to the \texttt{box} key itself; without it, \texttt{box} keeps the counts in force. % % \end{function} % % \begin{function}[added=2023-05-31,updated=2026-11-07]{\wires} % \begin{syntax} % \cs{wires}\oarg{\TikZ\ keys}\marg{connectivity} % \end{syntax} % % To connect boxes, you can use the \cmd\wires\ macro. % Its optional argument is \TikZ\ styling for the wires. % Its mandatory argument lists them, grouped by the node they start from: \meta{node} \texttt{= \{} \meta{anchor} \texttt{=} \meta{node}\texttt{.}\meta{anchor}\texttt{,} \dots\ \texttt{\}}. % % Wires start and end at the sides of nodes (\texttt{north}, \texttt{east}, \texttt{south} and \texttt{west}), at their corners (\texttt{north east} and so on), and at the ports of boxes (such as \texttt{east.1}). % They leave sides and ports straight out, and corners diagonally. % Other anchors, such as \texttt{center}, have no direction, so wires can't start or end there. % Every entry is a wire: a node can be listed twice, and an anchor can carry several wires, both going out and coming in. % % The boundary of the picture is a node too, called \texttt{@}: wires to its sides are the loose ends of the diagram. % They go straight out to the side they are wired to, or bend towards it. % % \begin{tcblisting}{example} % \begin{tikzpicture}[scale=0.6] % \node[box=0/2/0/1] (A) at (-2, 0) {A}; % \node[box=0/1/0/2] (B) at (+2, 0) {B}; % \node[box=0/1/0/1] (C) at ( 0,+1) {C}; % \node[box=0/1/0/1] (D) at ( 0,-1) {D}; % \wires{ % A = { east.1 = C.west % , east.2 = D.west % , west = @.west }, % B = { east = @.east }, % C = { east = B.west.1 }, % D = { east = B.west.2 }, % } % \end{tikzpicture} % \end{tcblisting} % % \end{function} % % The boundary surrounds the picture as it is when \cmd\wires\ is first called, with a margin, and later calls in the same picture share it: so draw all nodes before the wires. % A wire across the boundary runs through its middle. % To place one elsewhere, as an identity beside a box, wire a coordinate to the boundary both ways. % Place the coordinate with a path, as below, since \cs{coordinate} doesn't count towards the size of the picture, and the boundary might not reach it. % % \begin{tcblisting}{example} % \begin{tikzpicture} % \node[box=1/0/1/0] (f) at (0,0) {f}; % \path (0.8,0) coordinate (i); % \wires{ % f = { north.1 = @.north % , south.1 = @.south }, % i = { north = @.north % , south = @.south }, % } % \end{tikzpicture} % \end{tcblisting} % % The keys of a call apply to all its wires, and come after the angles that \cmd\wires\ computes, so \texttt{out} and \texttt{in} override them. % To style some wires differently, call \cmd\wires\ more than once. % % \begin{tcblisting}{example} % \begin{tikzpicture} % \node[box=1/0/1/0] (f) at (0,0) {f}; % \node[box=1/0/1/0] (g) at (0,1.5) {g}; % \wires{ f = { south.1 = @.south } } % \wires[dashed]{ % f = { north.1 = g.south.1 } } % \wires{ g = { north.1 = @.north } } % \end{tikzpicture} % \end{tcblisting} % % Wires take their directions from the names of anchors, in the frame of the picture. % So pictures can be shifted and scaled, but not rotated or mirrored, and rotated nodes aren't supported. % % Up to version 0.2.1, \cmd\wires\ took a third argument listing the loose ends. % They are now wires to the boundary, and a third argument is no longer read. % % % \begin{function}[added=2023-05-31,updated=2026-11-07]{/pgf/dot} % % To split and join wires, you can use \texttt{dot}s. % Wires attach to their sides and corners, and leave corners diagonally. % Remember to have fun with styling wires. % % \begin{tcblisting}{example} % \begin{tikzpicture} % \node[box=0/1/0/2] (A) at ( 0,+1) {A}; % \node[box=0/2/0/1] (B) at ( 0,-1) {B}; % \node[dot] (x) at (+1, 0) {}; % \node[dot] (y) at (-1, 0) {}; % \wires[looseness=1.5,-stealth]{ % A = { east.1 = x.north % , west.1 = @.west }, % B = { east.1 = x.south % , east.2 = @.east }, % y = { north = A.west.2 % , south = B.west % , west = @.west }, % x = { east = @.east }, % } % \end{tikzpicture} % \end{tcblisting} % % \begin{tcblisting}{example} % \begin{tikzpicture} % \node[dot] (c) at (0,0) {}; % \node[dot] (m) at (0,1.2) {}; % \wires{ % c = { south = @.south % , north west = m.south west % , north east = m.south east }, % m = { north = @.north }, % } % \end{tikzpicture} % \end{tcblisting} % % \end{function} % % That's it. This is the package, for now. % % \end{documentation} % % ^^A=[ PACKAGE ]=============================================================== % % \begin{implementation} % % \section{Implementation} % % Open the \pkg{DocStrip} guards and set the internal namespace prefix (as per \LaTeX3 \pkg{DocStrip} convention). % \begin{macrocode} %<*package> %<@@=stridi> % \end{macrocode} % % Load the essential support (\pkg{expl3}, and \TikZ\ with the \texttt{fit} library that the boundary of \cmd\wires\ needs) \enquote{up-front}. % % \begin{macrocode} \RequirePackage{expl3}[2023/06/05] \RequirePackage{tikz}[2023/01/15] \usetikzlibrary{fit} % \end{macrocode} % % Identify the package and give the over all version information. % \begin{macrocode} \ProvidesExplPackage {string-diagrams} {2026/11/07} {0.3.0} {Draw string diagrams using TikZ} % \end{macrocode} % % \begin{macro}{ % /pgf/box ports north, % /pgf/box ports east, % /pgf/box ports south, % /pgf/box ports west, % /pgf/box ports, % } % \changes{0.3.0}{2026/11/07}{counts start at zero, and the shortcut checks that it gets four} % % Define high level keys to configure the number of ports on each side. % Boxes start with no ports. % The shortcut checks that it gets four counts, and sets the keys of the sides to their values; given nothing, it leaves them alone, so that the \texttt{box} style without counts keeps the ones in force. % \begin{macrocode} \msg_new:nnn { string-diagrams } { port-counts } { Box~ports~take~four~counts,~for~the~north,~east,~south~and~west~sides,~ as~in~1/2/1/2,~not~`#1'. } \seq_new:N \l_@@_port_counts_seq \pgfkeys{ /pgf/box~ports~north/.initial=0, /pgf/box~ports~east/.initial=0, /pgf/box~ports~south/.initial=0, /pgf/box~ports~west/.initial=0, /pgf/box~ports/.code={ \tl_if_blank:nF { #1 } { \seq_set_split:Nnn \l_@@_port_counts_seq { / } { #1 } \int_compare:nNnTF { \seq_count:N \l_@@_port_counts_seq } = { 4 } { \exp_args:Ne \pgfkeysalso { /pgf/box~ports~north = \seq_item:Nn \l_@@_port_counts_seq { 1 } , /pgf/box~ports~east = \seq_item:Nn \l_@@_port_counts_seq { 2 } , /pgf/box~ports~south = \seq_item:Nn \l_@@_port_counts_seq { 3 } , /pgf/box~ports~west = \seq_item:Nn \l_@@_port_counts_seq { 4 } , } } { \msg_error:nnn { string-diagrams } { port-counts } { #1 } } } }, } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_intersect_hv_lines_through:NN} % Calculates the intersection of two lines parallel to axes passing through given points on the plane. % \begin{arguments} % \item Point through which the vertical line passes % \item Point through which the horizontal line passes % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_intersect_hv_lines_through:NN { \pgfextractx { \pgf@xa } { #1 } \pgfextracty { \pgf@ya } { #2 } \pgfpoint { \pgf@xa } { \pgf@ya } } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_subdivide_segment:nNNNNN} % Defines the port anchors of a side of a box: equally spaced points on a segment. % Anchors are defined globally, by the first box with enough ports on that side, so each one checks the port count of the box it is asked of. % The box is named in the error when an anchor of a named node is asked for, but not while placing a node by one of its own anchors. % \begin{arguments} % \item Side of the box % \item Points count % \item Point containing the x coordinate of the starting point % \item Point containing the y coordinate of the starting point % \item Point containing the x coordinate of the ending point % \item Point containing the y coordinate of the ending point % \end{arguments} % \begin{macrocode} \msg_new:nnn { string-diagrams } { missing-port } { #1~has~no~port~#2.#3,~since~its~#2~side~has~#4. } \cs_new_protected:Nn \@@_subdivide_segment:nNNNNN { \int_step_inline:nnnn { #2 } { -1 } { 1 } { \cs_if_exist:cTF { pgf@anchor@box@#1.##1 } { \prg_break: } { \prg_do_nothing: } \cs_new_nopar:cpn { pgf@anchor@box@#1.##1 } { \int_compare:nNnTF { ##1 } > { #2 } { \msg_error:nneeee { string-diagrams } { missing-port } { \cs_if_exist:NTF \pgfreferencednodename { Box~`\pgfreferencednodename' } { This~box } } { #1 } { ##1 } { #2 } \pgfpointlineattime { 0.5 } { \@@_intersect_hv_lines_through:NN { #3 } { #4 } } { \@@_intersect_hv_lines_through:NN { #5 } { #6 } } } { \pgfmathdivide { 2 * ##1 - 1 } { 2 * #2 } \pgfpointlineattime { \pgfmathresult } { \@@_intersect_hv_lines_through:NN { #3 } { #4 } } { \@@_intersect_hv_lines_through:NN { #5 } { #6 } } } } } } % \end{macrocode} % \end{macro} % % \begin{macro}[no-user-doc]{box} % \changes{0.2.0}{2023/06/12}{make ports configurable through \TikZ\ keys} % \changes{0.3.0}{2026/11/07}{ports a box doesn't have are an error} % % Define a rectangular shape with configurable ports. % \begin{macrocode} \pgfdeclareshape{box}{ % Inherit all the structure of rectangle \inheritsavedanchors[from=rectangle] \clist_map_inline:nn { north~west, north, north~east, west, center, east, mid~west, mid, mid~east, base~west, base, base~east, south~west, south, south~east, } { \inheritanchor[from=rectangle]{#1} } \inheritanchorborder[from=rectangle] \inheritbackgroundpath[from=rectangle] % Dump port counts into saved macros \savedmacro\portsnorth {\pgfmathtruncatemacro\portsnorth{\pgfkeysvalueof{/pgf/box~ports~north}}} \savedmacro\portseast {\pgfmathtruncatemacro\portseast{\pgfkeysvalueof{/pgf/box~ports~east}}} \savedmacro\portssouth {\pgfmathtruncatemacro\portssouth{\pgfkeysvalueof{/pgf/box~ports~south}}} \savedmacro\portswest {\pgfmathtruncatemacro\portswest{\pgfkeysvalueof{/pgf/box~ports~west}}} % Add ports definitions to shape definition \expandafter\pgfutil@g@addto@macro\csname pgf@sh@s@box\endcsname{ \@@_subdivide_segment:nNNNNN { north } { \portsnorth } { \southwest } { \northeast } { \northeast } { \northeast } \@@_subdivide_segment:nNNNNN { east } { \portseast } { \northeast } { \northeast } { \northeast } { \southwest } \@@_subdivide_segment:nNNNNN { south } { \portssouth } { \southwest } { \southwest } { \northeast } { \southwest } \@@_subdivide_segment:nNNNNN { west } { \portswest } { \southwest } { \northeast } { \southwest } { \southwest } } } % \end{macrocode} % \end{macro} % % \begin{macro}{/pgf/box} % \changes{0.2.0}{2023/06/12}{acts as a shortcut for setting port counts} % \changes{0.3.0}{2026/11/07}{without counts, keeps the ones in force} % % Define style to draw boxes. % \begin{macrocode} \ExplSyntaxOff \tikzset{ box/.default={}, box/.style args={#1}{ shape=box, draw, inner sep=.5em, minimum width=2em, minimum height=2em, execute at begin node=$, execute at end node=$, /pgf/box ports=#1, }, } \ExplSyntaxOn % \end{macrocode} % \end{macro} % % \begin{macro}{/pgf/dot} % % Define style to draw dots. % \begin{macrocode} \ExplSyntaxOff \tikzset{ dot/.style={ shape=circle, fill, inner sep=0, minimum width=0.4em, }, } \ExplSyntaxOn % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_clamp_to_node:n} % Moves the point in the internal \texttt{pgf} variables to the nearest point of the bounding rectangle of the given node, if it lies outside it. % \begin{macrocode} \dim_new:N \l_@@_x_dim \dim_new:N \l_@@_y_dim \cs_new_protected:Nn \@@_clamp_to_node:n { \dim_set:Nn \l_@@_x_dim { \pgf@x } \dim_set:Nn \l_@@_y_dim { \pgf@y } \pgfpointanchor { #1 } { north~east } \dim_set:Nn \l_@@_x_dim { \dim_min:nn { \l_@@_x_dim } { \pgf@x } } \dim_set:Nn \l_@@_y_dim { \dim_min:nn { \l_@@_y_dim } { \pgf@y } } \pgfpointanchor { #1 } { south~west } \dim_set:Nn \l_@@_x_dim { \dim_max:nn { \l_@@_x_dim } { \pgf@x } } \dim_set:Nn \l_@@_y_dim { \dim_max:nn { \l_@@_y_dim } { \pgf@y } } \pgfpoint { \l_@@_x_dim } { \l_@@_y_dim } } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_calctarget:nnnn} % Performs the core calculation for \macro{\@@_anchors_to_proj:Nnnnn}. % The result is stored in the internal \texttt{pgf} variables. % A wire from a corner to a side named in it, such as south west to south or west, ends offset along its diagonal. % A bend that would end past a corner of the boundary ends at that corner. % \begin{arguments} % \item source node % \item source anchor % \item target node (can be boundary) % \item target anchor % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_calctarget:nnnn { \pgfpointanchor{#1}{#2} \pgf@xa=\pgf@x \pgf@ya=\pgf@y \pgfpointanchor{#3}{#4} \pgf@xb=\pgf@x \pgf@yb=\pgf@y \regex_match_case:nn { { \A north ; north \ west \b } { \pgfpoint { \pgf@xa-.618*(\pgf@yb-\pgf@ya) } { \pgf@yb } } { \A north ; north \ east \b } { \pgfpoint { \pgf@xa+.618*(\pgf@yb-\pgf@ya) } { \pgf@yb } } { \A south ; south \ west \b } { \pgfpoint { \pgf@xa-.618*(\pgf@ya-\pgf@yb) } { \pgf@yb } } { \A south ; south \ east \b } { \pgfpoint { \pgf@xa+.618*(\pgf@ya-\pgf@yb) } { \pgf@yb } } { \A east ; north \ east \b } { \pgfpoint { \pgf@xb } { \pgf@ya+.618*(\pgf@xb-\pgf@xa) } } { \A east ; south \ east \b } { \pgfpoint { \pgf@xb } { \pgf@ya-.618*(\pgf@xb-\pgf@xa) } } { \A west ; north \ west \b } { \pgfpoint { \pgf@xb } { \pgf@ya+.618*(\pgf@xa-\pgf@xb) } } { \A west ; south \ west \b } { \pgfpoint { \pgf@xb } { \pgf@ya-.618*(\pgf@xa-\pgf@xb) } } { \A north ; north \b } { \pgfpoint { \pgf@xa } { \pgf@yb } } { \A north ; south \b } { \pgfpoint { \pgf@xa } { \pgf@yb } } { \A south ; north \b } { \pgfpoint { \pgf@xa } { \pgf@yb } } { \A south ; south \b } { \pgfpoint { \pgf@xa } { \pgf@yb } } { \A north ; east \b } { \pgfpoint { \pgf@xa+.618*(\pgf@yb-\pgf@ya) } { \pgf@yb } } { \A south ; east \b } { \pgfpoint { \pgf@xa-.618*(\pgf@yb-\pgf@ya) } { \pgf@yb } } { \A north ; west \b } { \pgfpoint { \pgf@xa-.618*(\pgf@yb-\pgf@ya) } { \pgf@yb } } { \A south ; west \b } { \pgfpoint { \pgf@xa+.618*(\pgf@yb-\pgf@ya) } { \pgf@yb } } { \A east ; east \b } { \pgfpoint { \pgf@xb } { \pgf@ya } } { \A east ; west \b } { \pgfpoint { \pgf@xb } { \pgf@ya } } { \A west ; east \b } { \pgfpoint { \pgf@xb } { \pgf@ya } } { \A west ; west \b } { \pgfpoint { \pgf@xb } { \pgf@ya } } { \A east ; north \b } { \pgfpoint { \pgf@xb } { \pgf@ya+.618*(\pgf@xb-\pgf@xa) } } { \A west ; north \b } { \pgfpoint { \pgf@xb } { \pgf@ya-.618*(\pgf@xb-\pgf@xa) } } { \A east ; south \b } { \pgfpoint { \pgf@xb } { \pgf@ya-.618*(\pgf@xb-\pgf@xa) } } { \A west ; south \b } { \pgfpoint { \pgf@xb } { \pgf@ya+.618*(\pgf@xb-\pgf@xa) } } } { #4 ; #2 } \@@_clamp_to_node:n { #3 } } % \end{macrocode} % \end{macro} % % \begin{variable}{\l_@@_angle_int} % Scratch variable for \cmd{\@@_anchor_to_angle:Nnn}. % \begin{macrocode} \int_new:N \l_@@_angle_int % \end{macrocode} % \end{variable} % % \begin{macro}{\@@_anchor_to_angle:Nnn} % Calculates in/out angle given a node/anchor pair. % Wires leave sides and their ports straight out, and corners diagonally. % Accounts for when the node is the boundary. % The result is stored in the given variable. % \begin{arguments} % \item token list variable % \item node % \item anchor % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_anchor_to_angle:Nnn { \regex_match_case:nnTF { { \A north \ east \b } { \int_set:Nn \l_@@_angle_int { 45 } } { \A north \ west \b } { \int_set:Nn \l_@@_angle_int { 135 } } { \A south \ east \b } { \int_set:Nn \l_@@_angle_int { -45 } } { \A south \ west \b } { \int_set:Nn \l_@@_angle_int { -135 } } { \A north \b } { \int_set:Nn \l_@@_angle_int { 90 } } { \A east \b } { \int_set:Nn \l_@@_angle_int { 0 } } { \A south \b } { \int_set:Nn \l_@@_angle_int { -90 } } { \A west \b } { \int_set:Nn \l_@@_angle_int { 180 } } } { #3 } { \str_if_eq:nnTF { @ } { #2 } { \int_sub:Nn \l_@@_angle_int { 180 } } { } \tl_set:Nf #1 { \int_use:N \l_@@_angle_int } } { \tl_clear:N #1 } } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_anchors_to_proj:Nnnnn} % Calculates a target point given two node/anchor pairs. % The first pair can be on the boundary. % The result is stored in the given token list variable. % \begin{arguments} % \item token list variable % \item source node % \item source anchor % \item target node % \item target anchor % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_anchors_to_proj:Nnnnn { \regex_match_case:nn { { \A @ \. } { \@@_calctarget:nnnn { #4 } { #5 } { #2 } { #3 } \tl_set:Ne #1 { \dim_use:N \pgf@x, \dim_use:N \pgf@y } } { } { \tl_set:Nn #1 { #2.#3 } } } { #2.#3 } } % \end{macrocode} % \end{macro} % % \begin{variable}{\l_@@_oangle_tl,\l_@@_iangle_tl,\l_@@_source_tl,\l_@@_target_tl} % Initialize scratch variables required by \cmd{\@@_draw_wire:nnnnn}. % \begin{macrocode} \tl_new:N \l_@@_oangle_tl \tl_new:N \l_@@_iangle_tl \tl_new:N \l_@@_source_tl \tl_new:N \l_@@_target_tl % \end{macrocode} % \end{variable} % % \begin{macro}{\@@_check_end:nnN} % Raises an error if a wire end has no angle to leave at, saying whether it has no anchor or an anchor off the sides. % \begin{arguments} % \item node % \item anchor % \item token list variable holding the angle % \end{arguments} % \begin{macrocode} \msg_new:nnn { string-diagrams } { end-without-anchor } { Wire~end~`#1'~has~no~anchor,~so~the~wire~is~not~drawn. } \msg_new:nnn { string-diagrams } { end-off-sides } { Wire~end~`#1.#2'~is~not~on~a~side,~so~the~wire~is~not~drawn:~ wires~end~at~north,~east,~south~or~west,~or~at~ports. } \cs_new_protected:Nn \@@_check_end:nnN { \tl_if_empty:NT #3 { \tl_if_blank:nTF { #2 } { \msg_error:nnn { string-diagrams } { end-without-anchor } { #1 } } { \msg_error:nnnn { string-diagrams } { end-off-sides } { #1 } { #2 } } } } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_draw_wire:nnnnn} % Draws a wire, unless an end has no angle to leave at: then it raises an error for each such end. % \begin{arguments} % \item \TikZ\ keys % \item source node % \item source anchor % \item target node % \item target anchor % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_draw_wire:nnnnn { \@@_anchor_to_angle:Nnn \l_@@_oangle_tl { #2 } { #3 } \@@_anchor_to_angle:Nnn \l_@@_iangle_tl { #4 } { #5 } \@@_check_end:nnN { #2 } { #3 } \l_@@_oangle_tl \@@_check_end:nnN { #4 } { #5 } \l_@@_iangle_tl \bool_lazy_or:nnF { \tl_if_empty_p:N \l_@@_oangle_tl } { \tl_if_empty_p:N \l_@@_iangle_tl } { \@@_anchors_to_proj:Nnnnn \l_@@_source_tl { #2 } { #3 } { #4 } { #5 } \@@_anchors_to_proj:Nnnnn \l_@@_target_tl { #4 } { #5 } { #2 } { #3 } \path [ out={\tl_use:N \l_@@_oangle_tl}, in= {\tl_use:N \l_@@_iangle_tl}, draw, #1, ] (\tl_use:N \l_@@_source_tl) to (\tl_use:N \l_@@_target_tl) ; } } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_draw_boundary:,\@@_draw_boundary_around:nn} % Creates the boundary node around the picture so far, or around the origin if nothing has been drawn yet (as pgf does, it tells an empty picture by its untouched bounds). % A picture gets a single boundary: later calls of \cmd\wires\ in it reuse the first one, found by the picture that pgf records for each node. % \begin{arguments} % \item north east corner to surround % \item south west corner to surround % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_draw_boundary: { % NOTE: the space keeps docstrip from turning the @@ of the name of node @ into the module prefix. \bool_lazy_and:nnF { \cs_if_exist_p:c { pgf@sh@pi@ @ } } { \str_if_eq_p:ee { \use:c { pgf@sh@pi@ @ } } { \pgfpictureid } } { \dim_compare:nNnTF { \pgf@picmaxx } = { -16000pt } { \@@_draw_boundary_around:nn { 0pt, 0pt } { 0pt, 0pt } } { \@@_draw_boundary_around:nn { current~bounding~box.north~east } { current~bounding~box.south~west } } } } \cs_new_protected:Nn \@@_draw_boundary_around:nn { \node [%draw, box=2/2/0/1, dashed, red, name=@, node~contents={}, fit={ ([shift={(+2em,+1em)}]#1) ([shift={(-2em,-1em)}]#2) }, ]; } % \end{macrocode} % \end{macro} % % \begin{variable}{\l_@@_split_seq} % Scratch variable for \cmd{\@@_split_node_anchor:NNn}. % \begin{macrocode} \seq_new:N \l_@@_split_seq % \end{macrocode} % \end{variable} % % \begin{macro}{\@@_split_node_anchor:NNn} % Parses a token list into a node/anchor pair. % \begin{arguments} % \item token list variable to store node % \item token list variable to store anchor % \item token list to be parsed % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_split_node_anchor:NNn { \regex_extract_once:nnN { ([^\.]*) \.? (.*) } { #3 } \l_@@_split_seq \seq_pop:NN \l_@@_split_seq #1 % discard match \seq_pop:NN \l_@@_split_seq #1 % keep 1st matching group (node) \seq_pop:NN \l_@@_split_seq #2 % keep 2nd matching group (anchor) } % \end{macrocode} % \end{macro} % % \begin{macro}[no-user-doc]{\exp_args:Nnnnoo} % This helper is required by \cmd{\@@_adj_map_function:nnN}. % \begin{macrocode} \exp_args_generate:n { nnnoo } % \end{macrocode} % \end{macro} % % \begin{variable}{\l_@@_target_node_tl,\l_@@_target_anchor_tl} % Scratch variables for \cmd{\@@_adj_map_wire:nNnnn}: the two parts of a target. % \begin{macrocode} \tl_new:N \l_@@_target_node_tl \tl_new:N \l_@@_target_anchor_tl % \end{macrocode} % \end{variable} % % \begin{macro}{\@@_adj_map_function:nnN} % Unrolls the nested dictionary of adjacencies into pairs and maps a function over them. % The given list of \TikZ\ keys is passed through to the function. % Entries are read one by one, in order, so a node listed twice keeps the wires of both entries, and an anchor listed twice fans out. % \begin{arguments} % \item \TikZ\ keys to pass through % \item nested dictionary of adjacencies % \item function to be mapped over pairs % \end{arguments} % \begin{macrocode} \msg_new:nnn { string-diagrams } { node-without-wires } { Node~`#1'~has~no~wires:~list~them~as~in~#1~=~\{~north~=~b.south~\}. } \cs_new_protected:Nn \@@_adj_map_function:nnN { \keyval_parse:nnn { \msg_error:nnn { string-diagrams } { node-without-wires } } { \@@_adj_map_node:nNnn { #1 } #3 } { #2 } } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_adj_map_node:nNnn} % Maps a function over the wires of one node. % \begin{arguments} % \item \TikZ\ keys to pass through % \item function to be mapped over pairs % \item node % \item dictionary of its wires % \end{arguments} % \begin{macrocode} \msg_new:nnn { string-diagrams } { end-without-target } { Wire~end~`#1.#2'~has~no~target:~give~it~one~as~in~#2~=~b.south. } \cs_new_protected:Nn \@@_adj_map_node:nNnn { \keyval_parse:nnn { \msg_error:nnnn { string-diagrams } { end-without-target } { #3 } } { \@@_adj_map_wire:nNnnn { #1 } #2 { #3 } } { #4 } } % \end{macrocode} % \end{macro} % % \begin{macro}{\@@_adj_map_wire:nNnnn} % Applies a function to one wire, splitting its target into node and anchor. % \begin{arguments} % \item \TikZ\ keys to pass through % \item function to be applied % \item source node % \item source anchor % \item target % \end{arguments} % \begin{macrocode} \cs_new_protected:Nn \@@_adj_map_wire:nNnnn { \@@_split_node_anchor:NNn \l_@@_target_node_tl \l_@@_target_anchor_tl { #5 } \exp_args:Nnnnoo #2 { #1 } { #3 } { #4 } { \l_@@_target_node_tl } { \l_@@_target_anchor_tl } } % \end{macrocode} % \end{macro} % % \begin{macro}{\wires} % \changes{0.2.1}{2023/06/13}{now correctly handles optional style parameter} % \changes{0.3.0}{2026/11/07}{loose ends are wires to the boundary node \texttt{@}, replacing the third argument} % \changes{0.3.0}{2026/11/07}{every listed wire is drawn, so nodes can be listed twice and anchors can fan out} % \changes{0.3.0}{2026/11/07}{wires leave corners diagonally} % \changes{0.3.0}{2026/11/07}{wire ends off the sides or without an anchor are errors} % Define our superstar. % \begin{macrocode} \NewDocumentCommand{\wires}{ O{} m } { \@@_draw_boundary: \@@_adj_map_function:nnN { #1 } { #2 } \@@_draw_wire:nnnnn } % \end{macrocode} % \end{macro} % % Close the \pkg{DocStrip} guards and call it a day. % \begin{macrocode} % % \end{macrocode} % % \end{implementation}