Add flow layout: cursor placement, left rail, declared band heights
Figures were positioned by hand-written offsets. Every gap was a magic number tuned against the content that happened to be there, so a label that grew two characters landed on the next tensor, and two stages started from two different x shared no rail. Both failures compile cleanly. Replace it with a cursor. Objects placed with an empty coordinate argument reserve their own width -- including a stack's offset sheets and a bracket's overhang -- and gaps are declared once (\stgutter, \strowgap, \stblockgap). The gap belongs to the object that follows it and the first object in a band gets none, so every band starts flush on a shared rail and gap=0pt states that two shards tile exactly. \stlink makes a connector's label a flow object, which is what removes the label-wider-than-its-arrow failure entirely. \stcol is a vertical sub-flow for a split along the contracted axis. \strow declares its height, so an object that does not fit -- or a column that does not add up to what it declared, and is therefore drawn off-center -- becomes a package warning, which build.sh fails on. Absolute placement is unchanged: passing a coordinate takes the original code path, and \sttrack folds a hand-placed node back into the cursor. All three golden examples and the new tests/flow.tex are converted and build clean.
This commit is contained in:
+287
-10
@@ -92,6 +92,247 @@
|
||||
line width=0.4pt},
|
||||
}
|
||||
|
||||
% --------------------------------------------------------- flow layout ------
|
||||
% Hand-placed absolute coordinates are the main source of layout bugs in these
|
||||
% figures. Every gap ends up a tuned magic number, so a label that grows by two
|
||||
% characters silently lands on the next tensor, and two stages started from two
|
||||
% different x share no rail. The cursor below removes both by construction:
|
||||
% gaps are declared once, positions are derived from the objects actually
|
||||
% drawn. Absolute placement still works -- pass a coordinate instead of `{}'.
|
||||
%
|
||||
% \ststage{S1}{stage heading} % anchored on the left rail
|
||||
% \strow{R1}{T} % band of height T units
|
||||
% \stface[role=q]{Q}{}{T}{d} % empty coord = place at the cursor
|
||||
% \stglyph{m}{$\times$}
|
||||
% \stface[role=w]{W}{}{d}{d}
|
||||
% \stcol{C}{2*d units} % a vertical pair in one band slot
|
||||
% \stface[role=r1]{Ca}{}{d}{d}
|
||||
% \stface[role=r2, gap=0pt]{Cb}{}{d}{d}
|
||||
% \stcolend
|
||||
% \strowend % fits the band, arms \stlane
|
||||
\newlength{\stgutter}\setlength{\stgutter}{6mm} % between objects in a band
|
||||
\newlength{\strowgap}\setlength{\strowgap}{3.5mm} % above a band
|
||||
\newlength{\stblockgap}\setlength{\stblockgap}{9mm} % above a stage heading
|
||||
\newlength{\stlinklen}\setlength{\stlinklen}{10mm} % bare connector reservation
|
||||
\newlength{\st@railx}\newlength{\st@cx}\newlength{\st@cy}
|
||||
\newlength{\st@ycur}\newlength{\st@bandh}\newlength{\st@tmpx}\newlength{\st@tmpy}
|
||||
% Column sub-flow state. Named st@v* rather than st@col*: \st@cols and \st@col
|
||||
% are already the column count and the resolved hue of a face.
|
||||
\newlength{\st@vx}\newlength{\st@vy}\newlength{\st@vw}\newlength{\st@vtop}
|
||||
\newlength{\st@vh}
|
||||
\newif\ifst@inrow
|
||||
\newif\ifst@incol
|
||||
\newif\ifst@first
|
||||
|
||||
\newcommand{\stlayoutreset}{%
|
||||
\global\st@railx=0pt \global\st@ycur=0pt
|
||||
\global\st@cx=0pt \global\st@cy=0pt \global\st@bandh=0pt
|
||||
\global\st@inrowfalse \global\st@incolfalse
|
||||
\gdef\st@rowlist{}\gdef\st@alllist{}\gdef\st@rowname{}%
|
||||
\gdef\st@collist{}\gdef\st@colname{}%
|
||||
\gdef\st@lastnode{}\gdef\st@linklabel{}\gdef\st@linkprev{}}
|
||||
\stlayoutreset
|
||||
\newcommand{\stleftrail}[1]{\global\st@railx=\dimexpr#1\relax}
|
||||
\newcommand{\stvgap}[1]{\global\advance\st@ycur by -\dimexpr#1\relax}
|
||||
|
||||
% The vertical cursor tracks the lowest ink so far, so the next band never has
|
||||
% to be positioned by eye.
|
||||
\newcommand{\st@lower}[1]{%
|
||||
\pgfextracty{\st@tmpy}{\pgfpointanchor{#1}{south}}%
|
||||
\ifdim\st@tmpy<\st@ycur \global\st@ycur=\st@tmpy \fi}
|
||||
\newcommand{\st@regall}[1]{\xdef\st@alllist{\st@alllist(#1)}}
|
||||
% Inside a column the items belong to the column, not to the band: they must not
|
||||
% terminate a pending connector, or the arrow would land on the first sheet of
|
||||
% the stack instead of on the column as a whole.
|
||||
\newcommand{\st@regrow}[1]{%
|
||||
\ifst@incol
|
||||
\xdef\st@collist{\st@collist(#1)}\st@regall{#1}%
|
||||
\else
|
||||
\xdef\st@rowlist{\st@rowlist(#1)}\st@regall{#1}%
|
||||
\st@drawpendinglink{#1}%
|
||||
\gdef\st@lastnode{#1}%
|
||||
\fi}
|
||||
|
||||
\newcommand{\st@needrow}[1]{%
|
||||
\ifst@inrow\else
|
||||
\PackageError{supertensor}{\string#1\space needs an open \string\strow}%
|
||||
{Flow placement only works between \string\strow\space and
|
||||
\string\strowend. Pass an explicit coordinate instead.}%
|
||||
\fi}
|
||||
|
||||
% The gap belongs to the object that follows it, and the first object in a band
|
||||
% gets none -- that is what puts every band's left edge on the same rail.
|
||||
\newcommand{\st@leadgap}[1]{%
|
||||
\ifst@first
|
||||
\global\st@firstfalse
|
||||
\else
|
||||
\ifst@incol\global\advance\st@vy by -\dimexpr#1\relax
|
||||
\else\global\advance\st@cx by \dimexpr#1\relax\fi
|
||||
\fi}
|
||||
|
||||
% Reserve #1 of horizontal space, hand back the center coordinate for it.
|
||||
\newcommand{\st@flow}[1]{%
|
||||
\dimen0=\dimexpr#1\relax
|
||||
\dimen2=\st@cx \advance\dimen2 by 0.5\dimen0
|
||||
\edef\st@pos{(\the\dimen2,\the\st@cy)}%
|
||||
\global\advance\st@cx by \dimen0}
|
||||
|
||||
% Reserve #2 of vertical space inside a column, hand back the center for it.
|
||||
% Items are centered on the column's own axis, so a narrow shard sits under a
|
||||
% wide one without an eyeballed offset.
|
||||
\newcommand{\st@vflow}[2]{%
|
||||
\dimen0=\dimexpr#1\relax \dimen4=\dimexpr#2\relax
|
||||
\dimen2=\st@vx \advance\dimen2 by 0.5\dimen0
|
||||
\dimen6=\st@vy \advance\dimen6 by -0.5\dimen4
|
||||
\edef\st@pos{(\the\dimen2,\the\dimen6)}%
|
||||
\global\advance\st@vy by -\dimen4
|
||||
\ifdim\dimen0>\st@vw \global\st@vw=\dimen0 \fi}
|
||||
|
||||
% A band declares its height, so an object that does not fit is a build
|
||||
% failure rather than something the reader discovers.
|
||||
\newcommand{\st@checkh}[2]{%
|
||||
\dimen0=\dimexpr#2\relax \advance\dimen0 by -\st@bandh
|
||||
\ifdim\dimen0>4mm
|
||||
\PackageWarning{supertensor}{Object `#1' overflows its band by
|
||||
\the\dimen0. Raise the height declared in \string\strow\space or give it
|
||||
its own band}%
|
||||
\fi}
|
||||
|
||||
\newcommand{\ststage}[2]{%
|
||||
\dimen0=\st@ycur \advance\dimen0 by -\stblockgap
|
||||
\node[st stage, anchor=north west] (#1) at (\the\st@railx,\the\dimen0) {#2};
|
||||
\st@regall{#1}\st@lower{#1}}
|
||||
|
||||
% \strow{name}{height in axis units or a declared axis}
|
||||
\newcommand{\strow}[2]{%
|
||||
\gdef\st@rowlist{}\gdef\st@rowname{#1}\gdef\st@lastnode{}%
|
||||
\gdef\st@linklabel{}\gdef\st@linkprev{}%
|
||||
\global\st@inrowtrue\global\st@firsttrue
|
||||
% Resolve through the ledger first: pgfmath's parser cannot digest
|
||||
% \stresolve's \ifcsname on its own.
|
||||
\edef\st@bandu{\stresolve{#2}}%
|
||||
\pgfmathsetlengthmacro{\st@bh}{\st@bandu*\stunit}%
|
||||
\global\st@bandh=\st@bh
|
||||
\global\st@cx=\st@railx
|
||||
\dimen0=\st@ycur \advance\dimen0 by -\strowgap \advance\dimen0 by -0.5\st@bandh
|
||||
\global\st@cy=\dimen0}
|
||||
|
||||
\newcommand{\strowend}{%
|
||||
\ifdefempty{\st@rowlist}%
|
||||
{\PackageWarning{supertensor}{Empty \string\strow\space `\st@rowname'}}%
|
||||
{\edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\st@rowlist}] (\st@rowname) {};}\st@do
|
||||
\stlane{\st@rowname}\st@regall{\st@rowname}\st@lower{\st@rowname}}%
|
||||
\global\st@inrowfalse}
|
||||
|
||||
% \stcol{name}{height} ... \stcolend -- a vertical sub-flow occupying one slot
|
||||
% of the band. This is what a split along a shared axis looks like: two shards
|
||||
% stacked with gap=0pt tile their parent exactly, which no pair of hand-tuned
|
||||
% y offsets can guarantee.
|
||||
\newcommand{\stcol}[2]{%
|
||||
\st@needrow{\stcol}%
|
||||
\ifst@incol
|
||||
\PackageError{supertensor}{Nested \string\stcol}%
|
||||
{Close the open column with \string\stcolend\space first.}%
|
||||
\fi
|
||||
\st@leadgap{\stgutter}%
|
||||
\gdef\st@collist{}\gdef\st@colname{#1}%
|
||||
\global\st@vx=\st@cx \global\st@vw=0pt
|
||||
\edef\st@colu{\stresolve{#2}}%
|
||||
\pgfmathsetlengthmacro{\st@ch}{\st@colu*\stunit}%
|
||||
\global\st@vh=\st@ch
|
||||
\dimen0=\st@cy \advance\dimen0 by 0.5\st@vh
|
||||
\global\st@vtop=\dimen0 \global\st@vy=\dimen0
|
||||
\global\st@incoltrue\global\st@firsttrue}
|
||||
|
||||
\newcommand{\stcolend}{%
|
||||
\ifdefempty{\st@collist}%
|
||||
{\PackageWarning{supertensor}{Empty \string\stcol\space `\st@colname'}%
|
||||
\global\st@incolfalse}%
|
||||
{\dimen0=\st@vtop \advance\dimen0 by -\st@vy % height actually consumed
|
||||
\global\st@incolfalse
|
||||
\edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\st@collist}] (\st@colname) {};}\st@do
|
||||
\global\advance\st@cx by \st@vw
|
||||
\global\st@firstfalse
|
||||
% The column is centered on the band using its DECLARED height, so a
|
||||
% mismatch is not just an overflow -- it is a column drawn off-center.
|
||||
\dimen2=\dimen0 \advance\dimen2 by -\st@vh
|
||||
\ifdim\dimen2<0pt \dimen2=-\dimen2 \fi
|
||||
\ifdim\dimen2>2mm
|
||||
\PackageWarning{supertensor}{Column `\st@colname' consumes
|
||||
\the\dimen0\space but declares \the\st@vh; it is drawn off-center.
|
||||
Fix the height in \string\stcol}%
|
||||
\fi
|
||||
\st@checkh{\st@colname}{\the\dimen0}%
|
||||
\st@regrow{\st@colname}}}
|
||||
|
||||
% A node in the flow: an operator glyph, a collective, a note. It reserves its
|
||||
% own width, which is the whole point -- `right=6mm of X' reserves nothing, so
|
||||
% the next face is free to land on top of it.
|
||||
\newcommand{\stnode}[3][]{%
|
||||
\st@needrow{\stnode}\st@leadgap{\stgutter}%
|
||||
\ifst@incol
|
||||
\node[#1, anchor=north west] (#2) at (\the\st@vx,\the\st@vy) {#3};
|
||||
\pgfextractx{\st@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\advance\st@tmpx by -\st@vx
|
||||
\ifdim\st@tmpx>\st@vw \global\st@vw=\st@tmpx \fi
|
||||
\pgfextracty{\st@tmpy}{\pgfpointanchor{#2}{south}}%
|
||||
\global\st@vy=\st@tmpy
|
||||
\else
|
||||
\node[#1, anchor=west] (#2) at (\the\st@cx,\the\st@cy) {#3};
|
||||
\pgfextractx{\st@tmpx}{\pgfpointanchor{#2}{east}}%
|
||||
\global\st@cx=\st@tmpx
|
||||
\fi
|
||||
\st@regrow{#2}}
|
||||
% Not \stop: that name is already taken by plain TeX.
|
||||
\newcommand{\stglyph}[2]{\stnode[st op]{#1}{#2}}
|
||||
\newcommand{\stcomm}[2]{\stnode[st comm]{#1}{#2}}
|
||||
\newcommand{\stgap}[1]{\global\advance\st@cx by \dimexpr#1\relax}
|
||||
|
||||
% \stlink{name}{label} -- a connector whose LABEL is a flow object. The label
|
||||
% reserves its own width and the arrows are drawn to the neighbours once the
|
||||
% right-hand one exists, so a label can never be wider than its connector.
|
||||
\newcommand{\stlink}[2]{%
|
||||
\st@needrow{\stlink}%
|
||||
\xdef\st@linkprev{\st@lastnode}%
|
||||
\ifblank{#2}%
|
||||
{\gdef\st@linklabel{}\stgap{\stlinklen}}%
|
||||
{\st@leadgap{\stgutter}%
|
||||
\node[st note, anchor=west, inner sep=1pt] (#1) at (\the\st@cx,\the\st@cy) {#2};
|
||||
\pgfextractx{\st@tmpx}{\pgfpointanchor{#1}{east}}%
|
||||
\global\st@cx=\st@tmpx
|
||||
\gdef\st@linklabel{#1}\st@regall{#1}}}
|
||||
\newcommand{\st@drawpendinglink}[1]{%
|
||||
\ifdefempty{\st@linkprev}{}{%
|
||||
\begin{pgfonlayer}{stbg}
|
||||
\ifdefempty{\st@linklabel}%
|
||||
{\draw[st arrow] (\st@linkprev.east) -- (#1.west);}%
|
||||
{\draw[st arrow] (\st@linkprev.east) -- (\st@linklabel.west);
|
||||
\draw[st arrow] (\st@linklabel.east) -- (#1.west);}%
|
||||
\end{pgfonlayer}
|
||||
\gdef\st@linkprev{}\gdef\st@linklabel{}}}
|
||||
|
||||
% Everything drawn through the flow, as one node -- for the meaning box anchor.
|
||||
\newcommand{\stbbox}[1]{%
|
||||
\edef\st@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={\st@alllist}] (#1) {};}\st@do}
|
||||
|
||||
% Escape hatch: fold a hand-placed node into the bounding box and push the
|
||||
% vertical cursor below it, so the next band still knows where the ink ends.
|
||||
\newcommand{\sttrack}[1]{\st@regall{#1}\st@lower{#1}}
|
||||
|
||||
% \sttopformula{name}{math} -- the formula line, centered on everything drawn
|
||||
% so far. Call it AFTER the bands: a formula placed first can only be centered
|
||||
% on a figure whose width is not known yet, which is how the top line ends up
|
||||
% visibly off-center.
|
||||
\newcommand{\sttopformula}[2]{%
|
||||
\stbbox{st@bbt}%
|
||||
% For more than one line, wrap the math in amsmath's `gathered': TikZ's own
|
||||
% \\ handling does not survive the group \stformula puts around the content.
|
||||
\node[above=6mm of st@bbt, anchor=south] (#1) {\stformula{#2}};
|
||||
\st@regall{#1}}
|
||||
|
||||
% ------------------------------------------------------------- faces --------
|
||||
\newif\ifst@bracket
|
||||
\newif\ifst@border
|
||||
@@ -102,10 +343,13 @@
|
||||
pattern/.store in=\st@pattern,
|
||||
data/.store in=\st@data,
|
||||
level/.store in=\st@level,
|
||||
gap/.store in=\st@gap,
|
||||
bracket/.is if=st@bracket,
|
||||
border/.is if=st@border,
|
||||
tiles/.is if=st@tiles,
|
||||
role=neutral, pattern=dense, data={}, level=2,
|
||||
% gap= is the space BEFORE this object, replacing the standing gutter.
|
||||
% gap=0pt is how shards are made to tile their parent exactly.
|
||||
role=neutral, pattern=dense, data={}, level=2, gap=\stgutter,
|
||||
bracket=false, border=true, tiles=true,
|
||||
}
|
||||
|
||||
@@ -153,17 +397,36 @@
|
||||
% `role=\st@role' back through pgfkeys would define \st@role in terms of
|
||||
% itself and hang the run.
|
||||
\newcommand{\st@facecore}[2]{%
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#1) at #2 {};
|
||||
\st@basenode{#1}{#2}%
|
||||
\st@facebody{#1}}
|
||||
|
||||
% Shared entry for cursor placement: reserve the width, check the band.
|
||||
\newcommand{\st@flowbegin}[3]{%
|
||||
\st@needrow{\stface}\st@leadgap{\st@gap}%
|
||||
\ifst@incol \st@vflow{#2}{#3}\else \st@flow{#2}\st@checkh{#1}{#3}\fi}
|
||||
|
||||
% Neither the TikZ path parser nor calc expands a macro sitting where it
|
||||
% expects `('. Everything the cursor computes therefore goes through \edef and
|
||||
% reaches the parser as literal text.
|
||||
\newcommand{\st@coordat}[2]{\coordinate (#1) at #2;}
|
||||
\newcommand{\st@basenode}[2]{%
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#1) at #2 {};}
|
||||
|
||||
% \stface[keys]{name}{center coord}{rows}{cols}
|
||||
% rows/cols accept a declared axis name or a raw integer. Height <- rows,
|
||||
% width <- cols, always: a matrix face a x b never renders sideways.
|
||||
% An empty coord places the face at the row cursor. The bracket decoration is
|
||||
% drawn outside the node box, so its 3.4mm on each side is reserved too.
|
||||
\newcommand{\stface}[5][]{%
|
||||
\begingroup
|
||||
\st@setup{#1}{#4}{#5}%
|
||||
\st@facecore{#2}{#3}%
|
||||
\ifblank{#3}{%
|
||||
\ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@w+6.8mm}\else\let\st@tw\st@w\fi
|
||||
\st@flowbegin{#2}{\st@tw}{\st@h}%
|
||||
\edef\st@do{\noexpand\st@facecore{#2}{\st@pos}}\st@do
|
||||
\st@regrow{#2}%
|
||||
}{\st@facecore{#2}{#3}}%
|
||||
\endgroup}
|
||||
|
||||
% \stindexface[keys]{name}{center coord}{rows}{cols}{entries}
|
||||
@@ -175,8 +438,11 @@
|
||||
\newcommand{\stindexface}[6][]{%
|
||||
\begingroup
|
||||
\st@setup{#1}{#4}{#5}%
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#2) at #3 {};
|
||||
\ifblank{#3}%
|
||||
{\ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@w+6.8mm}\else\let\st@tw\st@w\fi
|
||||
\st@flowbegin{#2}{\st@tw}{\st@h}%
|
||||
\edef\st@do{\noexpand\st@basenode{#2}{\st@pos}}\st@do}%
|
||||
{\st@basenode{#2}{#3}}%
|
||||
\foreach \st@e [count=\st@z from 0] in {#6} {%
|
||||
\pgfmathtruncatemacro{\st@ii}{div(\st@z,\st@cols)+1}%
|
||||
\pgfmathtruncatemacro{\st@jj}{mod(\st@z,\st@cols)+1}%
|
||||
@@ -196,6 +462,7 @@
|
||||
\draw[draw=black!60, line width=0.5pt, rounded corners=1pt]
|
||||
(#2.south west) rectangle (#2.north east);
|
||||
\fi
|
||||
\ifblank{#3}{\st@regrow{#2}}{}%
|
||||
\endgroup}
|
||||
|
||||
\newcommand{\st@facebody}[1]{%
|
||||
@@ -250,9 +517,17 @@
|
||||
\st@setup{#1}{#4}{#5}%
|
||||
\pgfmathtruncatemacro{\st@back}{#6-1}%
|
||||
\pgfmathsetlengthmacro{\st@dx}{1.3mm}%
|
||||
\coordinate (#2-c) at ($#3+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$);
|
||||
\node[inner sep=0pt, outer sep=0pt, minimum width=\st@w, minimum height=\st@h]
|
||||
(#2-front) at (#2-c) {};
|
||||
% The offset sheets are part of the object: reserve their extent too, or the
|
||||
% neighbour is spaced against the front sheet and lands on the back ones.
|
||||
\ifblank{#3}%
|
||||
{\pgfmathsetlengthmacro{\st@tw}{\st@w+\st@back*\st@dx}%
|
||||
\pgfmathsetlengthmacro{\st@th}{\st@h+\st@back*\st@dx}%
|
||||
\ifst@bracket\pgfmathsetlengthmacro{\st@tw}{\st@tw+6.8mm}\fi
|
||||
\st@flowbegin{#2}{\st@tw}{\st@th}%
|
||||
\edef\st@do{\noexpand\st@coordat{#2-c}%
|
||||
{($\st@pos+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$)}}\st@do}%
|
||||
{\coordinate (#2-c) at ($#3+(-0.5*\st@back*\st@dx,-0.5*\st@back*\st@dx)$);}%
|
||||
\st@basenode{#2-front}{(#2-c)}%
|
||||
\ifnum\st@back>0
|
||||
% Ascending loop, descending index: `{\macro,...,1}' cannot infer its
|
||||
% direction from an unexpanded macro and runs away.
|
||||
@@ -269,6 +544,7 @@
|
||||
\node[inner sep=0pt, outer sep=0pt,
|
||||
fit={(#2-front) ($(#2-front.north east)+(\st@back*\st@dx,\st@back*\st@dx)$)}]
|
||||
(#2) {};
|
||||
\ifblank{#3}{\st@regrow{#2}}{}%
|
||||
\endgroup}
|
||||
|
||||
% ------------------------------------------------- symbol / shape captions ---
|
||||
@@ -286,7 +562,8 @@
|
||||
{\node[st sym, below=1.6mm of #1] (#1-sym) {#2};}%
|
||||
{\node[st sym, anchor=north]
|
||||
at ($(#1.center |- \st@lane.south)+(0,-1.6mm)$) (#1-sym) {#2};}%
|
||||
\node[st shape, below=0.6mm of #1-sym] (#1-shape) {#3};}
|
||||
\node[st shape, below=0.6mm of #1-sym] (#1-shape) {#3};%
|
||||
\st@regall{#1-shape}\st@lower{#1-shape}}
|
||||
\newcommand{\stcaptiontop}[2]{%
|
||||
\node[st sym, above=1.6mm of #1] (#1-top) {#2};}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user