Align with superfig/superderive convention. Two signatures in one figure can now coexist without colliding on the hardcoded name.
844 lines
37 KiB
TeX
844 lines
37 KiB
TeX
%% supertensor.sty -- shape-aware tensor figure toolkit
|
|
%% Geometry invariants are enforced by construction: every face is sized from a
|
|
%% declared axis length, so equal shapes get equal edges, a x a is a square, and
|
|
%% a transpose physically swaps width and height.
|
|
%%
|
|
%% Options: cjk load ctex with the portable fandol fontset (XeLaTeX)
|
|
%% en English rail labels in the meaning box (default: zh)
|
|
\NeedsTeXFormat{LaTeX2e}
|
|
\ProvidesPackage{supertensor}[2026/08/05 v0.2 shape-aware tensor figure toolkit]
|
|
|
|
\newif\ifst@cjk\st@cjkfalse
|
|
\newif\ifst@en\st@enfalse
|
|
\DeclareOption{cjk}{\st@cjktrue}
|
|
\DeclareOption{en}{\st@entrue}
|
|
\DeclareOption{zh}{\st@enfalse}
|
|
\ProcessOptions\relax
|
|
|
|
\RequirePackage{amsmath}
|
|
\RequirePackage{amssymb}
|
|
\RequirePackage{xcolor}
|
|
\RequirePackage{array}
|
|
\RequirePackage{etoolbox}
|
|
\RequirePackage{xstring}
|
|
\RequirePackage{tikz}
|
|
\usetikzlibrary{calc,positioning,arrows.meta,backgrounds,fit,shapes.geometric,%
|
|
decorations.pathreplacing,decorations.markings}
|
|
|
|
\ifst@cjk
|
|
\RequirePackage[UTF8,fontset=fandol]{ctex}
|
|
\fi
|
|
|
|
% ---------------------------------------------------------------- layers ----
|
|
% Connectors on the background, tensors on main, all text on the foreground.
|
|
\pgfdeclarelayer{stbg}
|
|
\pgfdeclarelayer{stfg}
|
|
\pgfsetlayers{stbg,main,stfg}
|
|
|
|
% ---------------------------------------------------------------- palette ---
|
|
% Muted, mid-chroma paper colors. Do not add saturated primaries.
|
|
\definecolor{stTeal}{HTML}{4F8FA5}
|
|
\definecolor{stOrange}{HTML}{EE995B}
|
|
\definecolor{stCoral}{HTML}{C95B5B}
|
|
\definecolor{stViolet}{HTML}{8A74B5}
|
|
\definecolor{stGray}{HTML}{85898F}
|
|
\definecolor{stInk}{HTML}{1A1A1A}
|
|
|
|
% Role registry: draw macros take a ROLE, never a color, so one tensor role
|
|
% keeps one hue across every stage of the figure.
|
|
% \stsetrole{X}{stTeal} -> role "X" is teal everywhere. Repeating the same
|
|
% declaration is harmless; changing it would make one role change meaning
|
|
% halfway through the figure, so report it as a dirty-build warning.
|
|
\newcommand{\stsetrole}[2]{%
|
|
\edef\st@newrole{#2}%
|
|
\ifcsname st@role@#1\endcsname
|
|
\edef\st@oldrole{\csname st@role@#1\endcsname}%
|
|
\ifx\st@oldrole\st@newrole\else
|
|
\PackageWarning{supertensor}{Role `#1' was already mapped to
|
|
`\st@oldrole' and cannot be remapped to `\st@newrole'}%
|
|
\fi
|
|
\else
|
|
\expandafter\gdef\csname st@role@#1\endcsname{#2}%
|
|
\fi}
|
|
% Expandable on purpose: usable inside \edef. Undeclared roles fall back to
|
|
% neutral gray and are reported at the end of the run.
|
|
\newcommand{\strole}[1]{%
|
|
\ifcsname st@role@#1\endcsname\csname st@role@#1\endcsname\else stGray\fi}
|
|
\newcommand{\stcheckrole}[1]{%
|
|
\ifcsname st@role@#1\endcsname\else
|
|
\PackageWarning{supertensor}{Undeclared role `#1' -- drawn in neutral gray.
|
|
Declare it with \string\stsetrole\space so the hue budget stays visible}%
|
|
\fi}
|
|
\stsetrole{neutral}{stGray}
|
|
|
|
% Three separated lightness levels. Never render data cells below level 1.
|
|
\newcommand{\stlevelpct}[1]{\ifcase#1 0\or30\or55\or80\else55\fi}
|
|
|
|
% ------------------------------------------------------------ geometry ------
|
|
% The geometry ledger, made executable. Declare each symbolic axis once:
|
|
% \stdim{d}{8} \stdim{dh}{4}
|
|
% then every face built from `d' has the same physical edge, everywhere.
|
|
\newlength{\stunit}\setlength{\stunit}{4.6mm}
|
|
\newlength{\sttilegap}\setlength{\sttilegap}{0.5mm}
|
|
\newcommand{\stdim}[2]{%
|
|
\edef\st@newdim{#2}%
|
|
\ifcsname st@dim@#1\endcsname
|
|
\edef\st@olddim{\csname st@dim@#1\endcsname}%
|
|
\ifx\st@olddim\st@newdim\else
|
|
\PackageWarning{supertensor}{Axis `#1' was already declared as
|
|
`\st@olddim' cells and cannot be redeclared as `\st@newdim'}%
|
|
\fi
|
|
\else
|
|
\expandafter\gdef\csname st@dim@#1\endcsname{#2}%
|
|
\fi}
|
|
\newcommand{\stresolve}[1]{\ifcsname st@dim@#1\endcsname\csname st@dim@#1\endcsname\else#1\fi}
|
|
|
|
% --------------------------------------------------------- type hierarchy ---
|
|
\newcommand{\stformula}[1]{{\large #1}}
|
|
\newcommand{\ststagelabel}[1]{{\small\bfseries\color{black!55}#1}}
|
|
\newcommand{\stoperator}[1]{{\Large #1}}
|
|
\newcommand{\stsymfont}[1]{{\small #1}}
|
|
\newcommand{\stshapefont}[1]{{\scriptsize\color{black!55}#1}}
|
|
\newcommand{\stprose}[1]{{\small #1}}
|
|
|
|
\tikzset{
|
|
st sym/.style = {font=\small, text=stInk, inner sep=1pt},
|
|
st shape/.style = {font=\scriptsize, text=black!55, inner sep=1pt},
|
|
st stage/.style = {font=\small\bfseries, text=black!55, inner sep=2pt},
|
|
st op/.style = {font=\Large, text=stInk, inner sep=2pt},
|
|
st note/.style = {font=\scriptsize, text=black!55, inner sep=2pt},
|
|
st arrow/.style = {-{Stealth[length=2.2mm,width=1.6mm]}, draw=black!45, line width=0.5pt},
|
|
st comm/.style = {draw=black!45, fill=black!4, rounded corners=1.2pt,
|
|
font=\scriptsize, inner sep=3pt},
|
|
st brace/.style = {decorate, decoration={brace,amplitude=3pt}, draw=black!45,
|
|
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@ingroup
|
|
\newif\ifst@first
|
|
\newcount\st@gcount % members registered by the open group
|
|
\newif\ifst@ghascol % the open group contains a \stcol partition
|
|
|
|
\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 \global\st@ingroupfalse
|
|
\gdef\st@rowlist{}\gdef\st@alllist{}\gdef\st@rowname{}%
|
|
\gdef\st@collist{}\gdef\st@colname{}%
|
|
\gdef\st@grouplist{}\gdef\st@gname{}%
|
|
\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.
|
|
% A group behaves the same way: the wrapper, not the wrapped face, is what a
|
|
% connector may attach to. Otherwise the arrow ends inside the outline and
|
|
% crosses a box that is not its endpoint.
|
|
\newcommand{\st@regrow}[1]{%
|
|
\ifst@incol
|
|
\xdef\st@collist{\st@collist(#1)}\st@regall{#1}%
|
|
\else\ifst@ingroup
|
|
\xdef\st@grouplist{\st@grouplist(#1)}\st@regall{#1}%
|
|
\global\advance\st@gcount by 1
|
|
\else
|
|
\xdef\st@rowlist{\st@rowlist(#1)}\st@regall{#1}%
|
|
\st@drawpendinglink{#1}%
|
|
\gdef\st@lastnode{#1}%
|
|
\fi\fi}
|
|
% Bracket arms are drawn OUTSIDE the node box, so a fit over the node alone is
|
|
% smaller than the ink. Register the bracket's extreme coordinates with
|
|
% whatever fit will enclose the node, or a group outline (pad 1.6mm, overhang
|
|
% 2.2mm) is crossed by the bracket it claims to contain.
|
|
\newcommand{\st@regink}[1]{%
|
|
\ifst@incol \xdef\st@collist{\st@collist#1}%
|
|
\else\ifst@ingroup \xdef\st@grouplist{\st@grouplist#1}%
|
|
\else \xdef\st@rowlist{\st@rowlist#1}%
|
|
\fi\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}%
|
|
\ifst@ingroup\global\st@ghascoltrue\fi
|
|
\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}%
|
|
% Sub-flow members never terminate a pending connector (\st@regrow), so a
|
|
% link opened inside one would build cleanly and draw no arrow at all.
|
|
\ifst@ingroup
|
|
\PackageError{supertensor}{\string\stlink\space inside \string\stgroup}%
|
|
{A group member cannot terminate a connector, so the arrow would be
|
|
dropped silently. Close the group first; the link then attaches to the
|
|
outline itself.}%
|
|
\fi
|
|
\ifst@incol
|
|
\PackageError{supertensor}{\string\stlink\space inside \string\stcol}%
|
|
{A column item cannot terminate a connector. Close the column first;
|
|
the link then attaches to the column as a whole.}%
|
|
\fi
|
|
\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
|
|
\newif\ifst@tiles
|
|
\pgfkeys{
|
|
/st/face/.cd,
|
|
role/.store in=\st@role,
|
|
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,
|
|
% 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,
|
|
}
|
|
|
|
% Validate face keys before drawing. Unknown patterns used to fall through to
|
|
% dense, which produced a plausible but semantically false picture.
|
|
\newif\ifst@validpattern
|
|
\newcommand{\st@checkpattern}{%
|
|
\st@validpatternfalse
|
|
\foreach \st@known in {solid,dense,diag,band,lower,upper,causal,empty,data}{%
|
|
\IfStrEq{\st@pattern}{\st@known}{\global\st@validpatterntrue}{}}%
|
|
\ifst@validpattern\else
|
|
\PackageWarning{supertensor}{Unknown face pattern `\st@pattern'.
|
|
Use solid, dense, diag, band, lower, upper, causal, empty, or data}%
|
|
\fi}
|
|
\newcommand{\st@checklevel}{%
|
|
\IfInteger{\st@level}{%
|
|
\ifnum\st@level<0
|
|
\PackageWarning{supertensor}{Face level `\st@level' is outside 0--3}%
|
|
\def\st@level{2}%
|
|
\else\ifnum\st@level>3
|
|
\PackageWarning{supertensor}{Face level `\st@level' is outside 0--3}%
|
|
\def\st@level{2}%
|
|
\fi\fi
|
|
}{\PackageWarning{supertensor}{Face level `\st@level' is not an integer in 0--3}%
|
|
\def\st@level{2}}}
|
|
|
|
% pattern=data is a rectangular rows-by-cols array of digits 0--3. Validate
|
|
% all three facts before \StrChar reaches a missing or malformed cell.
|
|
\newcommand{\st@checkdata}{%
|
|
\IfStrEq{\st@pattern}{data}{%
|
|
\def\st@datacount{0}%
|
|
\foreach \st@drow [count=\st@di] in \st@data {\xdef\st@datacount{\st@di}}%
|
|
\ifnum\st@datacount=\st@rows\else
|
|
\PackageWarning{supertensor}{pattern=data has \st@datacount\space rows;
|
|
expected \st@rows}%
|
|
\fi
|
|
\foreach \st@drow in \st@data {%
|
|
\StrLen{\st@drow}[\st@dlen]%
|
|
\ifnum\st@dlen=\st@cols\else
|
|
\PackageWarning{supertensor}{Data row `\st@drow' has \st@dlen\space cells;
|
|
expected \st@cols}%
|
|
\fi
|
|
\edef\st@badchars{\st@drow}%
|
|
\StrSubstitute{\st@badchars}{0}{}[\st@badchars]%
|
|
\StrSubstitute{\st@badchars}{1}{}[\st@badchars]%
|
|
\StrSubstitute{\st@badchars}{2}{}[\st@badchars]%
|
|
\StrSubstitute{\st@badchars}{3}{}[\st@badchars]%
|
|
\ifdefempty{\st@badchars}{}{%
|
|
\PackageWarning{supertensor}{Data row `\st@drow' contains values outside 0--3}}%
|
|
}%
|
|
}{}}
|
|
|
|
% \st@hash{i}{j}{n} -> \st@hv in 0..n-1.
|
|
% Nested mods on purpose. Any polynomial in (i,j) reduced mod 3 is periodic
|
|
% with period 3 in BOTH directions, so a polynomial hash makes rows 1,2,4,5 of
|
|
% a face literally identical -- the eye reads that stripe as structure in the
|
|
% data. Composing coprime moduli (17, 19) pushes the period past any face we
|
|
% would ever draw.
|
|
\newcommand{\st@hash}[3]{%
|
|
\pgfmathtruncatemacro{\st@hv}%
|
|
{mod(mod(11*#1+7*#2,17)*mod(5*#1+13*#2,19)+3*#1+#2,#3)}}
|
|
|
|
% \st@cellfill{i}{j} -> \st@lv in 0..3 (0 = known zero, left unfilled)
|
|
\newcommand{\st@cellfill}[2]{%
|
|
\edef\st@p{\st@pattern}%
|
|
\IfStrEq{\st@p}{solid}{\def\st@lv{\st@level}}{%
|
|
\IfStrEq{\st@p}{diag}{\pgfmathtruncatemacro{\st@lv}{ifthenelse(#1==#2,3,0)}}{%
|
|
\IfStrEq{\st@p}{band}{\pgfmathtruncatemacro{\st@lv}{ifthenelse(abs(#1-#2)<=1,3,0)}}{%
|
|
\IfStrEq{\st@p}{empty}{\def\st@lv{0}}{%
|
|
\IfStrEq{\st@p}{lower}{%
|
|
\ifnum#1<#2 \def\st@lv{0}\else\st@hash{#1}{#2}{2}%
|
|
\pgfmathtruncatemacro{\st@lv}{\st@hv+2}\fi}{%
|
|
\IfStrEq{\st@p}{upper}{%
|
|
\ifnum#1>#2 \def\st@lv{0}\else\st@hash{#1}{#2}{2}%
|
|
\pgfmathtruncatemacro{\st@lv}{\st@hv+2}\fi}{%
|
|
\IfStrEq{\st@p}{causal}{%
|
|
\ifnum#1<#2 \def\st@lv{0}\else\st@hash{#1}{#2}{3}%
|
|
\pgfmathtruncatemacro{\st@lv}{\st@hv+1}\fi}{%
|
|
% default, including `dense'
|
|
\st@hash{#1}{#2}{3}\pgfmathtruncatemacro{\st@lv}{\st@hv+1}}}}}}}}}
|
|
|
|
% Shared setup: read keys, resolve the axis names through the geometry ledger.
|
|
\newcommand{\st@setup}[3]{%
|
|
\pgfkeys{/st/face/.cd,#1}%
|
|
\edef\st@rows{\stresolve{#2}}%
|
|
\edef\st@cols{\stresolve{#3}}%
|
|
\st@checkpattern
|
|
\st@checklevel
|
|
\st@checkdata
|
|
\stcheckrole{\st@role}%
|
|
\edef\st@col{\strole{\st@role}}%
|
|
\pgfmathsetlengthmacro{\st@w}{\st@cols*\stunit}%
|
|
\pgfmathsetlengthmacro{\st@h}{\st@rows*\stunit}}
|
|
|
|
% \st@facecore{name}{coord} -- draws one face from the already-resolved state.
|
|
% \ststack calls this directly rather than re-entering \stface: passing
|
|
% `role=\st@role' back through pgfkeys would define \st@role in terms of
|
|
% itself and hang the run.
|
|
\newcommand{\st@facecore}[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}%
|
|
\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}%
|
|
\ifst@bracket\st@regink{(#2-inkw)(#2-inke)}\fi
|
|
}{\st@facecore{#2}{#3}}%
|
|
\endgroup}
|
|
|
|
% \stindexface[keys]{name}{center coord}{rows}{cols}{entries}
|
|
% An INDEX face: the cells carry discrete symbols, not magnitudes. Entries are
|
|
% comma-separated in row-major order (rows*cols of them); `.' leaves a cell
|
|
% blank. Deliberately a different grammar from \stface -- outlined cells, no
|
|
% lightness ramp -- because an index that is drawn like a score invites the
|
|
% reader to compare 7 > 2 as if the numbers meant size.
|
|
\newcommand{\stindexface}[6][]{%
|
|
\begingroup
|
|
\st@setup{#1}{#4}{#5}%
|
|
\def\st@indexcount{0}%
|
|
\foreach \st@e [count=\st@zi] in {#6} {\xdef\st@indexcount{\st@zi}}%
|
|
\pgfmathtruncatemacro{\st@indexexpected}{\st@rows*\st@cols}%
|
|
\ifnum\st@indexcount=\st@indexexpected\else
|
|
\PackageWarning{supertensor}{Index face `#2' has \st@indexcount\space entries;
|
|
expected \st@indexexpected\space for \st@rows\space x \st@cols}%
|
|
\fi
|
|
\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}%
|
|
\edef\st@ee{\st@e}%
|
|
\IfStrEq{\st@ee}{.}{}{%
|
|
\draw[draw=\st@col!70, line width=0.4pt, rounded corners=0.7pt,
|
|
fill=\st@col!12]
|
|
($(#2.north west)+(\st@jj*\stunit-\stunit+0.5\sttilegap,%
|
|
-\st@ii*\stunit+\stunit-0.5\sttilegap)$)
|
|
rectangle
|
|
($(#2.north west)+(\st@jj*\stunit-0.5\sttilegap,%
|
|
-\st@ii*\stunit+0.5\sttilegap)$);
|
|
\node[font=\tiny, text=\st@col!85!black, inner sep=0pt]
|
|
at ($(#2.north west)+(\st@jj*\stunit-0.5\stunit,%
|
|
-\st@ii*\stunit+0.5\stunit)$) {\st@ee};}}%
|
|
\ifst@border
|
|
\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]{%
|
|
\def\st@n{#1}%
|
|
\ifst@tiles
|
|
\IfStrEq{\st@pattern}{data}{%
|
|
\foreach \st@row [count=\st@ii] in \st@data {%
|
|
\foreach \st@jj in {1,...,\st@cols} {%
|
|
\StrChar{\st@row}{\st@jj}[\st@c]%
|
|
\IfInteger{\st@c}{%
|
|
\ifnum\st@c>0
|
|
\ifnum\st@c<4 \st@tile{#1}{\st@ii}{\st@jj}{\st@c}\fi
|
|
\fi}{} }}%
|
|
}{%
|
|
\foreach \st@ii in {1,...,\st@rows} {%
|
|
\foreach \st@jj in {1,...,\st@cols} {%
|
|
\st@cellfill{\st@ii}{\st@jj}%
|
|
\ifnum\st@lv>0
|
|
\st@tile{#1}{\st@ii}{\st@jj}{\st@lv}%
|
|
\fi}}%
|
|
}%
|
|
\else
|
|
\fill[\st@col!\stlevelpct{\st@level}, rounded corners=1pt]
|
|
(#1.south west) rectangle (#1.north east);
|
|
\fi
|
|
\ifst@border
|
|
\draw[draw=black!60, line width=0.5pt, rounded corners=1pt]
|
|
(#1.south west) rectangle (#1.north east);
|
|
\fi
|
|
\ifst@bracket
|
|
\draw[black!55, line width=0.5pt]
|
|
($(#1.north west)+(-1.1mm,0.6mm)$) -- ++(-1.1mm,0)
|
|
-- ($(#1.south west)+(-2.2mm,-0.6mm)$) -- ++(1.1mm,0);
|
|
\draw[black!55, line width=0.5pt]
|
|
($(#1.north east)+(1.1mm,0.6mm)$) -- ++(1.1mm,0)
|
|
-- ($(#1.south east)+(2.2mm,-0.6mm)$) -- ++(-1.1mm,0);
|
|
% The true ink extent, for \st@regink: fits over the bare node undershoot
|
|
% the bracket by 2.2mm horizontally and 0.6mm vertically.
|
|
\coordinate (#1-inkw) at ($(#1.south west)+(-2.2mm,-0.6mm)$);
|
|
\coordinate (#1-inke) at ($(#1.north east)+(2.2mm,0.6mm)$);
|
|
\fi}
|
|
|
|
% \st@tile{face}{row}{col}{level} -- one rounded tile with a white gutter.
|
|
\newcommand{\st@tile}[4]{%
|
|
\fill[\st@col!\stlevelpct{#4}, rounded corners=0.7pt]
|
|
($(#1.north west)+(#3*\stunit-\stunit+0.5\sttilegap,%
|
|
-#2*\stunit+\stunit-0.5\sttilegap)$)
|
|
rectangle
|
|
($(#1.north west)+(#3*\stunit-0.5\sttilegap,%
|
|
-#2*\stunit+0.5\sttilegap)$);}
|
|
|
|
% \ststack[keys]{name}{center}{rows}{cols}{sheets}
|
|
% Leading axes become depth. Back sheets are outline-only and are included in
|
|
% the bounding box, so neighbours can be spaced against the real extent.
|
|
\newcommand{\ststack}[6][]{%
|
|
\begingroup
|
|
\st@setup{#1}{#4}{#5}%
|
|
\pgfmathtruncatemacro{\st@back}{#6-1}%
|
|
\pgfmathsetlengthmacro{\st@dx}{1.3mm}%
|
|
% 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.
|
|
\foreach \st@kk in {1,...,\st@back} {%
|
|
\pgfmathtruncatemacro{\st@k}{\st@back+1-\st@kk}%
|
|
\draw[draw=black!35, line width=0.4pt, rounded corners=1pt, fill=white]
|
|
($(#2-front.south west)+(\st@k*\st@dx,\st@k*\st@dx)$)
|
|
rectangle
|
|
($(#2-front.north east)+(\st@k*\st@dx,\st@k*\st@dx)$);}
|
|
\fi
|
|
\st@facebody{#2-front}%
|
|
% Braces are load-bearing: a bare coordinate expression contains a comma and
|
|
% would be split into two pgfkeys keys.
|
|
\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}%
|
|
\ifst@bracket\st@regink{(#2-front-inkw)(#2-front-inke)}\fi}{}%
|
|
\endgroup}
|
|
|
|
% ------------------------------------------------------------- grouping ----
|
|
% \stgroup[keys]{name} ... \stgroupend -- a thin rounded outline naming the
|
|
% objects drawn between them as one composite ("these three sheets are q";
|
|
% "these two shards are W"). A sub-flow, like \stcol, and for the same reason:
|
|
%
|
|
% - the members are drawn INSIDE the block, so a group can only ever wrap
|
|
% adjacent objects -- one that reached across the band would swallow
|
|
% whatever sat in between;
|
|
% - the group, not the last member, terminates a pending \stlink and sources
|
|
% the next one, so an arrow lands on the outline instead of ending inside
|
|
% it and crossing a border that is not its endpoint;
|
|
% - the padding is reserved on both sides, so the neighbour cannot land
|
|
% tangent to the outline.
|
|
%
|
|
% This is the one place a tensor-colored outline is allowed, because here the
|
|
% outline IS the object being drawn -- see style.md.
|
|
\newlength{\stgrouppad}\setlength{\stgrouppad}{1.6mm}
|
|
\pgfkeys{
|
|
/st/group/.cd,
|
|
role/.store in=\st@grole,
|
|
pad/.store in=\st@gpad,
|
|
% No default role on purpose: a homogeneous group silently drawn in neutral
|
|
% gray violates the "outline in the composite's own hue" rule without any
|
|
% signal. A genuinely mixed group passes role=neutral explicitly.
|
|
role={}, pad=\stgrouppad,
|
|
}
|
|
\newcommand{\stgroup}[2][]{%
|
|
\st@needrow{\stgroup}%
|
|
\ifst@ingroup
|
|
\PackageError{supertensor}{Nested \string\stgroup}%
|
|
{Close the open group with \string\stgroupend\space first.}%
|
|
\fi
|
|
\ifst@incol
|
|
\PackageError{supertensor}{\string\stgroup\space inside \string\stcol}%
|
|
{A group reserves its padding along the band, not down the column. Wrap
|
|
the whole \string\stcol\space in the group instead.}%
|
|
\fi
|
|
\pgfkeys{/st/group/.cd,#1}%
|
|
\ifdefempty{\st@grole}{%
|
|
\PackageError{supertensor}{\string\stgroup\space `#2' has no role}%
|
|
{The outline is drawn in the hue of the composite it names. Pass
|
|
role=<declared role>, or role=neutral explicitly for a mixed group.}%
|
|
\def\st@grole{neutral}}{}%
|
|
\stcheckrole{\st@grole}%
|
|
% Snapshot everything the closing macro needs: the keys are re-read by the
|
|
% next face and the role ledger lookup must not be deferred.
|
|
\xdef\st@gname{#2}%
|
|
\xdef\st@gcolname{\strole{\st@grole}}%
|
|
\xdef\st@gpadval{\st@gpad}%
|
|
\st@leadgap{\stgutter}%
|
|
\global\advance\st@cx by \dimexpr\st@gpad\relax
|
|
\gdef\st@grouplist{}%
|
|
\global\st@gcount=0 \global\st@ghascolfalse
|
|
\global\st@ingrouptrue\global\st@firsttrue}
|
|
|
|
\newcommand{\stgroupend}{%
|
|
\global\st@ingroupfalse
|
|
\ifdefempty{\st@grouplist}%
|
|
{\PackageWarning{supertensor}{Empty \string\stgroup\space `\st@gname'}}%
|
|
{\global\advance\st@cx by \dimexpr\st@gpadval\relax
|
|
\begin{pgfonlayer}{stbg}
|
|
\edef\st@do{\noexpand\node[inner sep=\st@gpadval, outer sep=0pt,
|
|
draw=\st@gcolname!65, line width=0.5pt, rounded corners=2pt,
|
|
fit={\st@grouplist}] (\st@gname) {};}\st@do
|
|
\end{pgfonlayer}
|
|
\global\st@firstfalse
|
|
% A group must add information the members do not already carry: at least
|
|
% two adjacent objects, or one \stcol partition. Around a single face or
|
|
% stack the outline is decoration -- the stack already reads as one thing.
|
|
\ifnum\st@gcount<2
|
|
\ifst@ghascol\else
|
|
\PackageWarning{supertensor}{Group `\st@gname' wraps a single object.
|
|
Bind at least two members or one \string\stcol\space partition;
|
|
a lone stack or face already reads as one composite}%
|
|
\fi
|
|
\fi
|
|
% No \st@checkh: the overhang is 2*pad by construction, not driven by
|
|
% content, and the members were already checked against the band. Warning
|
|
% about it would only teach authors to inflate the declared band height.
|
|
% \strowend fits the group, so the caption lane still clears it.
|
|
\edef\st@do{\noexpand\st@regrow{\st@gname}}\st@do}}
|
|
|
|
% ------------------------------------------------- symbol / shape captions ---
|
|
% Symbol immediately under the block, shape on the next line. Both are reserved
|
|
% lanes: nothing else may be placed between a face and its caption.
|
|
% \stlane{node} declares a shared caption baseline: every later \stcaption
|
|
% hangs from the bottom of that node instead of from its own block, so symbols
|
|
% and shapes occupy two flat lanes even when the faces have different heights.
|
|
% Typical use: draw the row, \node[fit=(A)(B)(C)] (row) {};, \stlane{row}.
|
|
\def\st@lane{}
|
|
\newcommand{\stlane}[1]{\def\st@lane{#1}}
|
|
\newcommand{\stnolane}{\def\st@lane{}}
|
|
\newcommand{\stcaption}[3]{%
|
|
\ifdefempty{\st@lane}%
|
|
{\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};%
|
|
\st@regall{#1-shape}\st@lower{#1-shape}}
|
|
\newcommand{\stcaptiontop}[2]{%
|
|
\node[st sym, above=1.6mm of #1] (#1-top) {#2};}
|
|
|
|
% ------------------------------------------------------------ connectors ----
|
|
% Routed on the background layer so a connector can never cover a face.
|
|
\newcommand{\starrow}[3][]{%
|
|
\begin{pgfonlayer}{stbg}
|
|
\draw[st arrow,#1] (#2) -- (#3);
|
|
\end{pgfonlayer}}
|
|
\newcommand{\starrowlabel}[4][]{%
|
|
\begin{pgfonlayer}{stbg}
|
|
\draw[st arrow,#1] (#2) -- node[st note, above, fill=white, inner sep=1pt] {#4} (#3);
|
|
\end{pgfonlayer}}
|
|
|
|
% --------------------------------------------------------------- callout ---
|
|
% \stcallout{name}{text width}{anchor}{title}{body} -- a side note card hanging
|
|
% off the RIGHT edge of a finished band, top-aligned with it.
|
|
%
|
|
% It is deliberately hard to misuse. Inside an open band it is an error: a
|
|
% commentary card between two operands reads as a step in the computation,
|
|
% which is the antipattern layout.md names. Outside one it still pushes the
|
|
% vertical cursor below its own bottom edge, so a card taller than its band
|
|
% opens visible space rather than colliding with the next stage -- which is the
|
|
% signal that the text belongs in \stmeaningbox instead.
|
|
\newcommand{\stcallout}[5]{%
|
|
\ifst@inrow
|
|
\PackageError{supertensor}{\string\stcallout\space inside an open
|
|
\string\strow}%
|
|
{A callout is an aside, not an operand. Close the band with
|
|
\string\strowend\space first. If the text explains a step rather than
|
|
the band, it belongs in the stage heading or \string\stmeaningbox.}%
|
|
\fi
|
|
\node[anchor=north west, draw=black!18, fill=black!3, rounded corners=1.5pt,
|
|
inner xsep=7pt, inner ysep=6pt, text width=#2] (#1)
|
|
at ([xshift=\stgutter]#3.north east) {%
|
|
\scriptsize\raggedright
|
|
\ifblank{#4}{}{\textbf{#4}\par\vspace{1.5pt}}%
|
|
#5\par};
|
|
\sttrack{#1}}
|
|
|
|
% ------------------------------------------------------------ meaning box ---
|
|
\ifst@en
|
|
\def\st@lblaxes{Axes}\def\st@lblobj{Objects}\def\st@lblmech{Mechanism}
|
|
\else
|
|
\def\st@lblaxes{轴}\def\st@lblobj{对象}\def\st@lblmech{机制}
|
|
\fi
|
|
\newcommand{\stsetlabels}[3]{\def\st@lblaxes{#1}\def\st@lblobj{#2}\def\st@lblmech{#3}}
|
|
|
|
% \stmeaningbox{node name}{total width}{below-of anchor}{axes}{objects}{mechanism}
|
|
% One full-width, low-contrast box, one reading column, at most three rows.
|
|
% Leave a row empty to drop it.
|
|
\newcommand{\stmeaningbox}[6]{%
|
|
\node[below=4mm of #3, anchor=north,
|
|
draw=black!18, fill=black!3, rounded corners=1.5pt,
|
|
inner xsep=9pt, inner ysep=8pt, text width=#2] (#1) {%
|
|
\small
|
|
\setlength{\tabcolsep}{0pt}%
|
|
\renewcommand{\arraystretch}{1.25}%
|
|
\begin{tabular}{@{}p{\st@raillen}@{\hspace{7pt}}p{\dimexpr#2-\st@raillen-7pt\relax}@{}}
|
|
% \ifblank, not \IfStrEq: the row content is math and CJK, and must not
|
|
% be expanded just to test for emptiness.
|
|
\ifblank{#4}{}{\textbf{\st@lblaxes} & \raggedright\arraybackslash #4 \\}
|
|
\ifblank{#5}{}{\textbf{\st@lblobj} & \raggedright\arraybackslash #5 \\}
|
|
\ifblank{#6}{}{\textbf{\st@lblmech} & \raggedright\arraybackslash #6 \\}
|
|
\end{tabular}};}
|
|
\newlength{\st@raillen}\setlength{\st@raillen}{3.2em}
|
|
\newcommand{\stsetrail}[1]{\setlength{\st@raillen}{#1}}
|
|
|
|
% ------------------------------------------------------------- signature ----
|
|
% Optional centered subject line, outside the meaning box, low contrast.
|
|
\newcommand{\stsignature}[2]{%
|
|
\node[below=2.2mm of #2, font=\scriptsize, text=black!45] (#2-sig) {#1};}
|
|
|
|
\endinput
|